@venizia/ignis-docs 0.2.0 → 0.2.1-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +14 -14
  2. package/content/best-practices/api-usage-examples.md +39 -19
  3. package/content/best-practices/architectural-patterns.md +22 -11
  4. package/content/best-practices/architecture-decisions.md +29 -16
  5. package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
  6. package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
  7. package/content/best-practices/code-style-standards/control-flow.md +33 -1
  8. package/content/best-practices/code-style-standards/documentation.md +28 -8
  9. package/content/best-practices/code-style-standards/function-patterns.md +15 -4
  10. package/content/best-practices/code-style-standards/index.md +7 -2
  11. package/content/best-practices/code-style-standards/naming-conventions.md +26 -2
  12. package/content/best-practices/code-style-standards/route-definitions.md +9 -5
  13. package/content/best-practices/code-style-standards/tooling.md +14 -1
  14. package/content/best-practices/code-style-standards/type-safety.md +39 -6
  15. package/content/best-practices/common-pitfalls.md +59 -35
  16. package/content/best-practices/contribution-workflow.md +6 -2
  17. package/content/best-practices/data-modeling.md +78 -62
  18. package/content/best-practices/deployment-strategies.md +56 -19
  19. package/content/best-practices/error-handling.md +182 -93
  20. package/content/best-practices/index.md +6 -0
  21. package/content/best-practices/performance-optimization.md +26 -13
  22. package/content/best-practices/security-guidelines.md +35 -9
  23. package/content/best-practices/testing-strategies.md +7 -2
  24. package/content/best-practices/troubleshooting-tips.md +27 -17
  25. package/content/extensions/components/api-reference.md +107 -322
  26. package/content/extensions/components/authentication/api.md +454 -603
  27. package/content/extensions/components/authentication/errors.md +121 -498
  28. package/content/extensions/components/authentication/index.md +88 -801
  29. package/content/extensions/components/authentication/usage.md +207 -956
  30. package/content/extensions/components/authorization/api.md +736 -656
  31. package/content/extensions/components/authorization/errors.md +168 -206
  32. package/content/extensions/components/authorization/index.md +82 -797
  33. package/content/extensions/components/authorization/usage.md +194 -527
  34. package/content/extensions/components/health-check.md +71 -243
  35. package/content/extensions/components/mail/api.md +504 -287
  36. package/content/extensions/components/mail/errors.md +73 -61
  37. package/content/extensions/components/mail/index.md +96 -467
  38. package/content/extensions/components/mail/usage.md +130 -172
  39. package/content/extensions/components/request-tracker.md +66 -173
  40. package/content/extensions/components/socket-io/api.md +195 -13
  41. package/content/extensions/components/socket-io/errors.md +3 -3
  42. package/content/extensions/components/socket-io/index.md +50 -337
  43. package/content/extensions/components/socket-io/usage.md +143 -26
  44. package/content/extensions/components/static-asset/api.md +410 -142
  45. package/content/extensions/components/static-asset/errors.md +110 -53
  46. package/content/extensions/components/static-asset/index.md +79 -608
  47. package/content/extensions/components/static-asset/usage.md +180 -300
  48. package/content/extensions/components/websocket/api.md +275 -399
  49. package/content/extensions/components/websocket/errors.md +47 -56
  50. package/content/extensions/components/websocket/index.md +74 -407
  51. package/content/extensions/components/websocket/usage.md +110 -341
  52. package/content/extensions/helpers/cron/index.md +51 -160
  53. package/content/extensions/helpers/crypto/index.md +62 -483
  54. package/content/extensions/helpers/crypto/reference.md +456 -0
  55. package/content/extensions/helpers/env/index.md +60 -178
  56. package/content/extensions/helpers/error/index.md +221 -207
  57. package/content/extensions/helpers/inversion/index.md +65 -556
  58. package/content/extensions/helpers/inversion/reference.md +522 -0
  59. package/content/extensions/helpers/kafka/admin.md +20 -1
  60. package/content/extensions/helpers/kafka/compile-binary.md +41 -35
  61. package/content/extensions/helpers/kafka/consumer.md +54 -20
  62. package/content/extensions/helpers/kafka/examples.md +21 -16
  63. package/content/extensions/helpers/kafka/index.md +80 -610
  64. package/content/extensions/helpers/kafka/producer.md +134 -5
  65. package/content/extensions/helpers/kafka/schema-registry.md +45 -70
  66. package/content/extensions/helpers/logger/hf-logger.md +193 -0
  67. package/content/extensions/helpers/logger/index.md +64 -563
  68. package/content/extensions/helpers/logger/pino.md +85 -0
  69. package/content/extensions/helpers/logger/reference.md +746 -0
  70. package/content/extensions/helpers/network/api.md +241 -195
  71. package/content/extensions/helpers/network/index.md +72 -530
  72. package/content/extensions/helpers/queue/index.md +72 -900
  73. package/content/extensions/helpers/queue/reference.md +467 -0
  74. package/content/extensions/helpers/redis/index.md +73 -645
  75. package/content/extensions/helpers/redis/reference.md +727 -0
  76. package/content/extensions/helpers/secrets/index.md +66 -0
  77. package/content/extensions/helpers/socket-io/api.md +305 -203
  78. package/content/extensions/helpers/socket-io/index.md +66 -432
  79. package/content/extensions/helpers/storage/api.md +564 -462
  80. package/content/extensions/helpers/storage/index.md +77 -573
  81. package/content/extensions/helpers/types/index.md +66 -499
  82. package/content/extensions/helpers/types/reference.md +650 -0
  83. package/content/extensions/helpers/uid/index.md +58 -227
  84. package/content/extensions/helpers/websocket/api.md +329 -216
  85. package/content/extensions/helpers/websocket/index.md +65 -503
  86. package/content/extensions/helpers/worker-thread/index.md +58 -396
  87. package/content/extensions/helpers/worker-thread/reference.md +428 -0
  88. package/content/guides/core-concepts/persistent/models.md +1 -1
  89. package/content/guides/core-concepts/persistent/search-typesense.md +3 -3
  90. package/content/guides/core-concepts/persistent/transactions.md +1 -1
  91. package/content/guides/core-concepts/secrets-vault.md +177 -0
  92. package/content/guides/core-concepts/services.md +1 -1
  93. package/content/guides/migrations/redis-helpers-migration.md +1 -1
  94. package/content/guides/migrations/unified-connectors-migration.md +2 -2
  95. package/content/guides/tutorials/ecommerce-api.md +3 -8
  96. package/content/references/base/application.md +1 -1
  97. package/content/references/base/connectors.md +79 -136
  98. package/content/references/base/datasources-reference.md +599 -0
  99. package/content/references/base/datasources.md +84 -444
  100. package/content/references/base/dependency-injection.md +17 -39
  101. package/content/references/base/filter-system/application-usage.md +69 -121
  102. package/content/references/base/filter-system/array-operators.md +12 -0
  103. package/content/references/base/filter-system/comparison-operators.md +12 -0
  104. package/content/references/base/filter-system/default-filter.md +136 -348
  105. package/content/references/base/filter-system/fields-order-pagination.md +38 -16
  106. package/content/references/base/filter-system/index.md +106 -257
  107. package/content/references/base/filter-system/json-filtering.md +12 -2
  108. package/content/references/base/filter-system/list-operators.md +16 -2
  109. package/content/references/base/filter-system/logical-operators.md +13 -0
  110. package/content/references/base/filter-system/null-operators.md +13 -0
  111. package/content/references/base/filter-system/pattern-matching.md +12 -0
  112. package/content/references/base/filter-system/quick-reference.md +11 -2
  113. package/content/references/base/filter-system/range-operators.md +12 -0
  114. package/content/references/base/filter-system/tips.md +70 -133
  115. package/content/references/base/filter-system/use-cases.md +156 -233
  116. package/content/references/base/middlewares.md +35 -21
  117. package/content/references/base/models-reference.md +886 -0
  118. package/content/references/base/models.md +80 -1452
  119. package/content/references/base/repositories/advanced.md +156 -192
  120. package/content/references/base/repositories/index.md +77 -650
  121. package/content/references/base/repositories/mixins.md +22 -18
  122. package/content/references/base/repositories/relations.md +123 -171
  123. package/content/references/base/repositories/soft-deletable.md +58 -56
  124. package/content/references/base/secrets.md +263 -0
  125. package/content/references/base/services.md +2 -2
  126. package/content/references/configuration/environment-variables.md +48 -4
  127. package/content/references/configuration/index.md +49 -31
  128. package/content/references/quick-reference.md +3 -16
  129. package/content/references/utilities/crypto.md +35 -76
  130. package/content/references/utilities/date.md +33 -73
  131. package/content/references/utilities/index.md +1 -1
  132. package/content/references/utilities/jsx-reference.md +298 -0
  133. package/content/references/utilities/jsx.md +82 -525
  134. package/content/references/utilities/module.md +29 -62
  135. package/content/references/utilities/parse.md +34 -64
  136. package/content/references/utilities/performance.md +33 -58
  137. package/content/references/utilities/promise.md +28 -62
  138. package/content/references/utilities/request.md +57 -218
  139. package/content/references/utilities/schema.md +43 -137
  140. package/content/references/utilities/statuses-reference.md +361 -0
  141. package/content/references/utilities/statuses.md +63 -667
  142. package/package.json +8 -8
@@ -1,10 +1,85 @@
1
- # Authorization -- API Reference
1
+ ---
2
+ title: Authorization Reference
3
+ description: Full option tables, binding keys, constants, enforcer internals, adapters, policy builders, and controller integration for the Authorization component
4
+ difficulty: advanced
5
+ ---
2
6
 
3
- > Architecture, enforcer internals, provider, registry, adapters, models, and middleware pipeline. See [Setup & Configuration](./) for initial setup.
7
+ # Authorization Reference
8
+
9
+ Every option, binding key, class, and method the Authorization component exposes. See the [Overview](./) for the guided introduction and [Usage](./usage) for task-oriented examples.
10
+
11
+ **Files:**
12
+
13
+ - [`packages/core/src/components/auth/authorize/`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize) - component, providers, enforcers, adapters, models, middleware
14
+ - [`packages/core/src/components/auth/base/abstract-auth-registry.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/base/abstract-auth-registry.ts) - `AbstractAuthRegistry` (shared with Authentication)
15
+ - [`packages/core/src/base/metadata/persistents.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/metadata/persistents.ts) - `@model` populating `AUTHORIZATION_SUBJECT`
16
+ - [`packages/core/src/helpers/inversion/mixins/model.mixin.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/helpers/inversion/mixins/model.mixin.ts) - `MetadataRegistry` authorize-settings queries
17
+
18
+ ## Import paths
19
+
20
+ ```typescript
21
+ import {
22
+ // Component & middleware
23
+ AuthorizeComponent, AuthorizationProvider, authorize,
24
+
25
+ // Registry
26
+ AuthorizationEnforcerRegistry,
27
+
28
+ // Enforcers
29
+ CasbinAuthorizationEnforcer,
30
+
31
+ // Adapters
32
+ BaseFilteredAdapter, ScopedCasbinAdapter,
33
+
34
+ // Scoped RBAC model
35
+ CASBIN_RBAC_DOMAIN_SCOPED_MODEL,
36
+
37
+ // Models
38
+ AuthorizationRole,
39
+
40
+ // Policy / permission catalog builders
41
+ AuthorizationPolicyBuilder, AuthorizationPermissionBuilder,
42
+
43
+ // Resource-hierarchy matcher (register on a custom Casbin model)
44
+ objectMatch,
45
+
46
+ // Constants
47
+ Authorization, AuthorizationActions, AuthorizationDecisions, AuthorizationDomainScopes,
48
+ AuthorizationPolicyVariants, AuthorizationRoles, AuthorizationEnforcerTypes,
49
+ CasbinEnforcerModelDrivers, CasbinEnforcerCachedDrivers, CasbinRuleVariants,
50
+ CasbinDomainMatchingFunctions,
51
+
52
+ // Binding keys
53
+ AuthorizeBindingKeys,
54
+ } from '@venizia/ignis';
55
+
56
+ import type {
57
+ // Core interfaces
58
+ IAuthorizeOptions, IAuthorizationEnforcer, IAuthorizationSpec, IAuthorizationRequest,
59
+ IAuthorizationRole, IAuthorizationDomainSource, TAuthorizationDomainResolver,
60
+
61
+ // Casbin options
62
+ ICasbinEnforcerOptions, ICasbinEnforcerCachedRedis,
63
+
64
+ // Adapter types
65
+ ICasbinPolicyFilter, ICasbinPolicySource, IScopedCasbinEntities, IScopedCasbinPolicyFilter,
66
+
67
+ // Function & utility types
68
+ TAuthorizeFn, TAuthorizationVoter, TAuthorizationConditions, TRegistryDescriptor,
69
+
70
+ // Model-based authorization metadata
71
+ IModelAuthorizeSettings,
72
+
73
+ // Value types (from TConstValue)
74
+ TAuthorizationAction, TAuthorizationDecision, TAuthorizationEnforcerType,
75
+ TCasbinEnforcerCachedDriver, TCasbinEnforcerModelDriver, TCasbinRuleVariant,
76
+ TCasbinDomainMatchingFunction, TAuthorizationPolicyVariant, TAuthorizationDomainScope,
77
+ } from '@venizia/ignis';
78
+ ```
4
79
 
5
80
  ## Architecture
6
81
 
7
- ### System Overview
82
+ ### System overview
8
83
 
9
84
  ```mermaid
10
85
  graph TB
@@ -22,22 +97,10 @@ graph TB
22
97
  Registry --> Casbin["CasbinAuthorizationEnforcer<br/>+ FilteredAdapter"]
23
98
  Registry --> Custom["Custom Enforcer"]
24
99
 
25
- Provider --> Pipeline["Request Pipeline"]
26
-
27
- subgraph Pipeline["7-Step Middleware Pipeline"]
28
- direction TB
29
- S1["1. Skip check"]
30
- S2["2. User check"]
31
- S3["3. Role shortcuts"]
32
- S4["4. Voters"]
33
- S5["5. Resolve enforcer"]
34
- S6["6. Build rules"]
35
- S7["7. Evaluate"]
36
- S1 --> S2 --> S3 --> S4 --> S5 --> S6 --> S7
37
- end
100
+ Provider --> Pipeline["7-Step Middleware Pipeline"]
38
101
  ```
39
102
 
40
- ### Middleware Pipeline Flowchart
103
+ ### Middleware pipeline
41
104
 
42
105
  ```mermaid
43
106
  flowchart TD
@@ -54,7 +117,8 @@ flowchart TD
54
117
  Voters -->|ABSTAIN / none| HasEnforcers{Enforcers registered?}
55
118
  HasEnforcers -->|No| Next6([next - skip, no enforcers])
56
119
  HasEnforcers -->|Yes| Resolve[Resolve enforcer by name]
57
- Resolve --> Cache{Rules cached?}
120
+ Resolve --> ResolveDomain["Resolve domain (if spec.domain or domainResolver)"]
121
+ ResolveDomain --> Cache{Rules cached?}
58
122
  Cache -->|Yes| Evaluate
59
123
  Cache -->|No| PType{principalType?}
60
124
  PType -->|Missing| E400[/400 principalType required/]
@@ -69,15 +133,11 @@ flowchart TD
69
133
  Default -->|DENY| E403b
70
134
  ```
71
135
 
72
- ### Class Hierarchy
136
+ ### Class hierarchy
73
137
 
74
138
  ```mermaid
75
139
  classDiagram
76
- class BaseHelper {
77
- +logger
78
- +scope
79
- }
80
-
140
+ class BaseHelper { +logger +scope }
81
141
  class AbstractAuthRegistry~TItem~ {
82
142
  #descriptors: Map
83
143
  #getBindingPrefix()* string
@@ -87,7 +147,6 @@ classDiagram
87
147
  #registerDescriptor(opts) void
88
148
  #resolveDescriptor(opts) TItem
89
149
  }
90
-
91
150
  class AuthorizationEnforcerRegistry {
92
151
  -instance$ AuthorizationEnforcerRegistry
93
152
  -configuredEnforcers: Set
@@ -97,7 +156,6 @@ classDiagram
97
156
  +resolveEnforcer(opts) Promise
98
157
  +resolveOptions() IAuthorizeOptions
99
158
  }
100
-
101
159
  class IAuthorizationEnforcer {
102
160
  <<interface>>
103
161
  +name: string
@@ -105,7 +163,6 @@ classDiagram
105
163
  +buildRules(opts) TRules
106
164
  +evaluate(opts) TAuthorizationDecision
107
165
  }
108
-
109
166
  class CasbinAuthorizationEnforcer {
110
167
  -pool: BasePoolHelper~Enforcer~
111
168
  -pendingLineFetches: Map
@@ -113,10 +170,9 @@ classDiagram
113
170
  +destroy() void
114
171
  +buildRules(opts) ICasbinRules
115
172
  +evaluate(opts) TAuthorizationDecision
116
- +invalidateUserCache(opts)?
173
+ +invalidateUserCache(opts)?
117
174
  +rebuildUserCache(opts)?
118
175
  }
119
-
120
176
  class BaseFilteredAdapter~TFilter~ {
121
177
  <<abstract>>
122
178
  #dataSource: ICasbinPolicySource
@@ -125,17 +181,10 @@ classDiagram
125
181
  +isFiltered() boolean
126
182
  #loadLines(opts) void
127
183
  }
128
-
129
184
  class ScopedCasbinAdapter {
130
185
  #entities: IScopedCasbinEntities
131
186
  +loadFilteredPolicy(model, filter) void
132
- #queryRoleAssignments(opts) lines + roleIds
133
- #queryMemberships(opts) string[]
134
- #queryGrants(opts) string[]
135
- #loadStructuralTrees() string[]
136
- #expandRoleClosure(opts) IdType[]
137
187
  }
138
-
139
188
  BaseHelper <|-- AbstractAuthRegistry
140
189
  AbstractAuthRegistry <|-- AuthorizationEnforcerRegistry
141
190
  IAuthorizationEnforcer <|.. CasbinAuthorizationEnforcer
@@ -144,155 +193,397 @@ classDiagram
144
193
  BaseFilteredAdapter <|-- ScopedCasbinAdapter
145
194
  ```
146
195
 
147
- ### Module File Layout
196
+ ### Module file layout
148
197
 
149
198
  ```
150
- auth/authorize/
199
+ components/auth/authorize/
151
200
  ├── adapters/
152
201
  │ ├── base-filtered.ts # BaseFilteredAdapter (thin abstract) + ICasbinPolicyFilter
153
202
  │ ├── scoped-casbin.adapter.ts # ScopedCasbinAdapter (generic edge-table reader)
154
- │ └── types.ts # IScopedCasbinEntities, IScopedCasbinTable
203
+ │ └── types.ts # IScopedCasbinEntities, IScopedCasbinTable, ICasbinPolicySource
155
204
  ├── common/
156
- │ ├── constants.ts # Authorization, AuthorizationActions, AuthorizationDecisions,
157
- # AuthorizationDomainScopes, AuthorizationPolicyVariants,
158
- # AuthorizationRoles, AuthorizationEnforcerTypes,
159
- # CasbinEnforcerCachedDrivers, CasbinEnforcerModelDrivers,
160
- # CasbinRuleVariants
161
- │ ├── object-match.ts # objectMatch resource-hierarchy matcher
162
- ├── keys.ts # AuthorizeBindingKeys
163
- │ ├── types.ts # IAuthorizeOptions, IAuthorizationEnforcer,
164
- │ │ # IAuthorizationSpec, ICasbinEnforcerOptions, etc.
165
- │ └── index.ts # Barrel export
205
+ │ ├── constants.ts # Authorization, Actions, Decisions, PolicyVariants, Roles, ...
206
+ ├── keys.ts # AuthorizeBindingKeys
207
+ ├── object-match.ts # objectMatch resource-hierarchy matcher
208
+ ├── permission-builder.ts # AuthorizationPermissionBuilder
209
+ ├── policy-builder.ts # AuthorizationPolicyBuilder
210
+ │ ├── resolve-request-domain.ts # resolveRequestDomain
211
+ └── types.ts # IAuthorizeOptions, IAuthorizationEnforcer, ICasbinEnforcerOptions, ...
166
212
  ├── enforcers/
167
213
  │ ├── casbin.enforcer.ts # CasbinAuthorizationEnforcer
168
214
  │ ├── enforcer-registry.ts # AuthorizationEnforcerRegistry (singleton)
169
- ├── models/
170
- │ │ ├── rbac-domain.model.ts # CASBIN_RBAC_DOMAIN_SCOPED_MODEL (scoped model string)
171
- │ │ └── index.ts
172
- │ └── index.ts # Barrel export
173
- ├── middlewares/
174
- │ └── authorize.middleware.ts # authorize() standalone function
175
- ├── models/
176
- │ ├── authorization-role.model.ts # AuthorizationRole
177
- │ └── index.ts
178
- ├── providers/
179
- │ └── authorization.provider.ts # AuthorizationProvider
180
- ├── component.ts # AuthorizeComponent
181
- └── index.ts # Barrel export (all submodules)
215
+ └── models/rbac-domain.model.ts # CASBIN_RBAC_DOMAIN_SCOPED_MODEL
216
+ ├── middlewares/authorize.middleware.ts # authorize() standalone function
217
+ ├── models/authorization-role.model.ts # AuthorizationRole
218
+ ├── providers/authorization.provider.ts # AuthorizationProvider
219
+ └── component.ts # AuthorizeComponent
182
220
  ```
183
221
 
184
- ### Tech Stack
185
-
186
- | Technology | Purpose |
187
- |------------|---------|
188
- | **Hono middleware** | Route-level authorization via `createMiddleware` from `hono/factory` |
189
- | **`casbin`** (optional) | External policy engine for Casbin enforcer. Peer dependency -- not bundled. |
190
- | **`@venizia/ignis-helpers`** | `BaseHelper` base class, `getError` for error creation, `HTTP` result codes |
191
- | **`@venizia/ignis-inversion`** | `IProvider` interface, `BindingScopes` for singleton registration |
192
-
193
- ### Design Decisions
222
+ ### Design decisions
194
223
 
195
224
  | Decision | Rationale |
196
225
  |----------|-----------|
197
- | **Enforcer-based** | Pluggable architecture -- swap between Casbin and custom enforcers without changing route configs |
198
- | **Registry + co-located options** | Enforcer class, name, type, and options are registered together -- no split configuration across two binding sites |
199
- | **Type-discriminated enforcers** | `type: 'casbin' \| 'custom'` in registry for type-safe options (`ICasbinEnforcerOptions` vs `unknown`) |
200
- | **Voter pattern** | Custom logic that short-circuits before the enforcer (Spring Security inspiration) |
201
- | **Rules caching** | Built rules cached on Hono context per-request -- avoids rebuilding for multi-spec routes |
202
- | **Registry singleton** | Mirrors `AuthenticationStrategyRegistry` pattern -- consistent with the codebase |
203
- | **Abstract base** | `AbstractAuthRegistry<T>` shared between authentication and authorization registries |
204
- | **Filtered adapter pattern** | `BaseFilteredAdapter` is a thin read-only base; subclasses implement `loadFilteredPolicy` for custom query backends |
205
- | **No-enforcer fallback** | When no enforcers are registered, the middleware skips authorization and calls `next()` instead of throwing -- prevents hard failures during development or gradual rollout |
226
+ | Enforcer-based | Pluggable architecture - swap Casbin for a custom enforcer without changing route configs |
227
+ | Registry + co-located options | Enforcer class, name, type, and options are registered together - no split configuration |
228
+ | Type-discriminated enforcers | `type: 'casbin' \| 'custom'` in registry constrains `options` (`ICasbinEnforcerOptions` vs `unknown`) |
229
+ | Voter pattern | Custom logic that short-circuits before the enforcer |
230
+ | Rules caching | Built rules cached on the Hono context per-request - avoids rebuilding for multi-spec routes |
231
+ | Registry singleton | Mirrors `AuthenticationStrategyRegistry` - shares `AbstractAuthRegistry<T>` |
232
+ | Filtered adapter pattern | `BaseFilteredAdapter` is a thin read-only base; subclasses implement only `loadFilteredPolicy` |
233
+ | No-enforcer fallback | No enforcers registered -> the middleware skips authorization and calls `next()` instead of throwing |
234
+ | Single edge table (scoped model) | `ScopedCasbinAdapter` reads one `PolicyDefinition` table for every edge type - no per-relation tables |
206
235
 
207
- ## Component Lifecycle
236
+ ## AuthorizeComponent
208
237
 
209
- The `AuthorizeComponent` extends `BaseComponent` and executes during its `binding()` method:
238
+ `AuthorizeComponent extends BaseComponent`. Its `binding()` runs at application startup:
210
239
 
211
240
  | Step | Action | Failure |
212
241
  |------|--------|---------|
213
- | 1 | Resolve `IAuthorizeOptions` from container via `AuthorizeBindingKeys.OPTIONS` | Throws `[AuthorizeComponent] No authorize options found` |
214
- | 2 | Call `bindAlwaysAllowRoles()` -- binds `alwaysAllowRoles` to `AuthorizeBindingKeys.ALWAYS_ALLOW_ROLES` if present | -- (skipped if no roles) |
242
+ | 1 | Resolve `IAuthorizeOptions` from the container via `AuthorizeBindingKeys.OPTIONS` | Throws `[AuthorizeComponent] No authorize options found` |
243
+ | 2 | `bindAlwaysAllowRoles()` - binds `alwaysAllowRoles` to `AuthorizeBindingKeys.ALWAYS_ALLOW_ROLES` if present | Skipped if no roles configured |
215
244
 
216
245
  ```typescript
217
246
  class AuthorizeComponent extends BaseComponent {
218
- constructor(
219
- @inject({ key: CoreBindings.APPLICATION_INSTANCE }) private application: BaseApplication,
220
- ) { ... }
221
-
247
+ constructor(@inject({ key: CoreBindings.APPLICATION_INSTANCE }) private application: BaseApplication);
222
248
  override binding(): ValueOrPromise<void>;
223
249
  private bindAlwaysAllowRoles(opts: { options: IAuthorizeOptions }): void;
224
250
  }
225
251
  ```
226
252
 
227
253
  > [!NOTE]
228
- > The component's role is minimal -- it validates that global options exist and binds `alwaysAllowRoles` for consumer access. Enforcer registration happens separately via `AuthorizationEnforcerRegistry.register()`.
254
+ > Enforcer registration is separate - `AuthorizeComponent` only validates global options. Register enforcers via `AuthorizationEnforcerRegistry.register()`.
229
255
 
230
- ## AbstractAuthRegistry
256
+ Source -> [`component.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/component.ts)
231
257
 
232
- Shared base class for both authentication and authorization registries. Provides descriptor storage, binding key generation, and DI resolution.
258
+ ## Binding keys
233
259
 
234
- ### TRegistryDescriptor
260
+ | Key | Constant | Type | Description |
261
+ |-----|----------|------|-------------|
262
+ | `@app/authorize/options` | `AuthorizeBindingKeys.OPTIONS` | `IAuthorizeOptions` | Global authorization options |
263
+ | `@app/authorize/always-allow-roles` | `AuthorizeBindingKeys.ALWAYS_ALLOW_ROLES` | `string[]` | Auto-bound by the component if present in options |
264
+ | `@app/authorize/enforcers/{name}/options` | `AuthorizeBindingKeys.enforcerOptions(name)` | `ICasbinEnforcerOptions \| unknown` | Per-enforcer options, auto-bound by the registry |
235
265
 
236
266
  ```typescript
237
- type TRegistryDescriptor<TItem> = {
238
- container: Container;
239
- targetClass: TClass<TItem>;
240
- };
267
+ class AuthorizeBindingKeys {
268
+ static readonly OPTIONS = '@app/authorize/options';
269
+ static readonly ALWAYS_ALLOW_ROLES = '@app/authorize/always-allow-roles';
270
+ static enforcerOptions(name: string): string {
271
+ return `@app/authorize/enforcers/${name}/options`;
272
+ }
273
+ }
241
274
  ```
242
275
 
243
- ### Class
276
+ `AuthorizeBindingKeys.enforcerOptions(name)` is called automatically by `AuthorizationEnforcerRegistry.register()` when `options` is provided; `CasbinAuthorizationEnforcer` injects its options from `AuthorizeBindingKeys.enforcerOptions('casbin')`.
277
+
278
+ Source -> [`common/keys.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/common/keys.ts)
279
+
280
+ ## Option interfaces
281
+
282
+ ### IAuthorizeOptions
283
+
284
+ Global settings, bound before registering `AuthorizeComponent`.
285
+
286
+ | Option | Type | Default | Description |
287
+ |--------|------|---------|-------------|
288
+ | `defaultDecision` | `TAuthorizationDecision` | - | **Required.** Decision applied when the enforcer returns `ABSTAIN` |
289
+ | `alwaysAllowRoles` | `string[]` | `[]` | Roles that bypass all authorization checks (global) |
290
+ | `domainResolver` | `TAuthorizationDomainResolver` | - | Fallback domain resolver used when a route's `spec.domain` is not set. Returns `{ type, id }` or `null` (-> `SYSTEM_WIDE`) |
244
291
 
245
292
  ```typescript
246
- abstract class AbstractAuthRegistry<TItem> extends BaseHelper {
247
- protected descriptors: Map<string, TRegistryDescriptor<TItem>>;
293
+ interface IAuthorizeOptions {
294
+ defaultDecision: TAuthorizationDecision;
295
+ alwaysAllowRoles?: string[];
296
+ domainResolver?: TAuthorizationDomainResolver;
297
+ }
298
+ ```
248
299
 
249
- constructor(opts: { scope: string });
300
+ ### ICasbinEnforcerOptions
250
301
 
251
- // Abstract -- subclass provides the binding key prefix
252
- protected abstract getBindingPrefix(): string;
302
+ Casbin-specific options, provided per-enforcer via `AuthorizationEnforcerRegistry.register()`.
253
303
 
254
- // Public API
255
- getKey(opts: { name: string }): string;
256
- getDefaultName(): string;
257
- reset(): void;
304
+ | Option | Type | Default | Description |
305
+ |--------|------|---------|-------------|
306
+ | `model` | `{ driver: 'file', definition } \| { driver: 'text', definition }` | - | **Required.** Casbin model (file path or inline text). For scoped RBAC, use `CASBIN_RBAC_DOMAIN_SCOPED_MODEL` |
307
+ | `cached` | `{ use: false } \| (ICasbinEnforcerCachedRedis & { use: true })` | - | **Required.** Caching configuration (Redis-only) |
308
+ | `adapter` | `Adapter` | - | Casbin adapter instance (e.g. `ScopedCasbinAdapter`) |
309
+ | `isScoped` | `boolean` | `false` | Enables the scoped model: 4-token `(sub, dom, obj, act)` requests; auto-registers `keyMatch` on `g` + `objectMatch` on the resource relation |
310
+ | `poolSize` | `number` | `16` | Pooled enforcers (each request enforces on its own borrowed instance) |
311
+ | `poolAcquireTimeoutMs` | `number` | `5000` | Max ms to wait for a free pooled enforcer before failing closed |
312
+ | `normalizePayloadFn` | `(opts) => { subject, resource, action, domain? }` | - | Custom (non-scoped) payload normalizer, run before evaluation |
313
+ | `domainMatching` | `{ roleDefinition: string; fn: TCasbinDomainMatchingFunction }` | - | Opt-in domain matching function for the flat model. **Not needed when `isScoped: true`** |
258
314
 
259
- // Protected internals
260
- protected registerDescriptor(opts: { container: Container; target: TClass<TItem>; name: string }): void;
261
- protected resolveDescriptor(opts: { name: string }): TItem;
315
+ ```typescript
316
+ interface ICasbinEnforcerOptions<E extends Env = Env, TAction = string, TResource = string, TAdapter = Adapter> {
317
+ model: { driver: 'file'; definition: string } | { driver: 'text'; definition: string };
318
+ cached: { use: false } | (ICasbinEnforcerCachedRedis & { use: true });
319
+ adapter?: TAdapter;
320
+ isScoped?: boolean;
321
+ poolSize?: number;
322
+ poolAcquireTimeoutMs?: number;
323
+ normalizePayloadFn?(opts: { user: IAuthUser; action: TAction; resource: TResource; context: TContext<E, string> }): {
324
+ subject: string; resource: string; action: string; domain?: string;
325
+ };
326
+ domainMatching?: { roleDefinition: string; fn: TCasbinDomainMatchingFunction };
262
327
  }
263
328
  ```
264
329
 
265
- ### Methods
330
+ > [!NOTE]
331
+ > `cached.options.expiresIn` must be `>= 10_000` ms (`MIN_EXPIRES_IN`). Caching is **Redis-only** - the in-memory driver was removed.
266
332
 
267
- | Method | Description | Throws |
268
- |--------|-------------|--------|
269
- | `getKey({ name })` | Builds binding key as `{prefix}.{name}` | `[getKey] Invalid name` if name is empty |
270
- | `getDefaultName()` | Returns the first registered descriptor's name (Map insertion order) | `[ClassName] No items registered` if none |
271
- | `registerDescriptor(opts)` | Stores `TRegistryDescriptor` in Map + binds class as `SINGLETON` in DI container | -- |
272
- | `resolveDescriptor({ name })` | Resolves instance from DI container by key | `Descriptor not found: {name}` or `Failed to resolve: {name}` |
273
- | `reset()` | Clears all descriptors from the Map | -- |
333
+ **Cache configuration (discriminated union):**
334
+
335
+ ```typescript
336
+ interface { use: false } // every request rebuilds the user's policy from the datasource
337
+
338
+ interface ICasbinEnforcerCachedRedis {
339
+ driver: 'redis';
340
+ options: {
341
+ connection: IRedisHelper;
342
+ expiresIn: number;
343
+ keyFn: (opts: { user: IAuthorizationUser }) => ValueOrPromise<string>;
344
+ };
345
+ }
346
+ ```
347
+
348
+ ### IAuthorizationSpec (route-level)
349
+
350
+ | Option | Type | Default | Description |
351
+ |--------|------|---------|-------------|
352
+ | `action` | `TAction` | - | **Required.** Action being performed (e.g. `'read'`, `'create'`) |
353
+ | `resource` | `TResource` | - | **Required.** Resource being accessed (e.g. `'Article'`) |
354
+ | `conditions` | `TAuthorizationConditions` | - | Key-value ABAC conditions. Plumbed into `request.conditions`; the built-in Casbin enforcer does not read it - only custom enforcers/voters can |
355
+ | `allowedRoles` | `string[]` | - | Roles that bypass the enforcer for this route |
356
+ | `voters` | `TAuthorizationVoter[]` | - | Custom voter functions for this route |
357
+ | `domain` | `IAuthorizationDomainSource \| TAuthorizationDomainResolver` | - | Per-route domain source for scoped RBAC. Omitted -> falls back to the global `domainResolver`, then `SYSTEM_WIDE` |
358
+
359
+ ```typescript
360
+ interface IAuthorizationSpec<E extends Env = Env, TAction = string, TResource = string> {
361
+ action: TAction;
362
+ resource: TResource;
363
+ conditions?: TAuthorizationConditions;
364
+ allowedRoles?: string[];
365
+ voters?: TAuthorizationVoter<E, TAction, TResource>[];
366
+ domain?: IAuthorizationDomainSource | TAuthorizationDomainResolver<E>;
367
+ }
368
+ ```
369
+
370
+ ### IAuthorizationDomainSource / TAuthorizationDomainResolver
371
+
372
+ ```typescript
373
+ interface IAuthorizationDomainSource {
374
+ from: 'param' | 'header' | 'query' | 'context';
375
+ key: string;
376
+ type: string; // domain type, e.g. 'Merchant'
377
+ }
378
+
379
+ type TAuthorizationDomainResolver<E extends Env = Env> = (opts: {
380
+ context: TContext<E, string>;
381
+ }) => ValueOrPromise<TNullable<{ type: string; id: IdType }>>;
382
+ ```
383
+
384
+ `resolveRequestDomain()` (`common/resolve-request-domain.ts`) turns either shape into a casbin domain string with this precedence: `spec.domain` (resolver, then declarative `readDeclarative()`) -> `IAuthorizeOptions.domainResolver` -> `AuthorizationDomainScopes.SYSTEM_WIDE`. `readDeclarative()` reads `context.req.param/header/query()` for `'param'|'header'|'query'`, or `context.get(key)` for `'context'`.
385
+
386
+ ### TAuthorizationConditions / TAuthorizationVoter / TAuthorizeFn
387
+
388
+ ```typescript
389
+ type TAuthorizationConditions<KeyType extends string | symbol = string | symbol, ValueType = string | number | boolean | null> =
390
+ Record<KeyType, ValueType>;
391
+
392
+ type TAuthorizationVoter<E extends Env = Env, TAction = string, TResource = string> = (opts: {
393
+ user: IAuthUser; action: TAction; resource: TResource; context: TContext<E, string>;
394
+ }) => ValueOrPromise<TAuthorizationDecision>;
395
+
396
+ type TAuthorizeFn<E extends Env = Env, TAction = string, TResource = string> = (opts: {
397
+ spec: IAuthorizationSpec<E, TAction, TResource>;
398
+ enforcerName?: string;
399
+ }) => MiddlewareHandler;
400
+ ```
401
+
402
+ ### IAuthorizationRequest
403
+
404
+ The request object built by the provider and passed to `evaluate()`.
274
405
 
275
- ### Subclass Binding Prefixes
406
+ ```typescript
407
+ interface IAuthorizationRequest<TAction = string, TResource = string> {
408
+ action: TAction;
409
+ resource: TResource;
410
+ conditions?: TAuthorizationConditions;
411
+ /** Resolved domain scope: `"<DomainType>_<id>"` (e.g. `"Merchant_7"`) or the `"SYSTEM_WIDE"` sentinel. */
412
+ domain?: string;
413
+ }
414
+ ```
415
+
416
+ Source -> [`common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/common/types.ts)
417
+
418
+ ## Constants
419
+
420
+ All constant classes follow the same pattern: static readonly values + `SCHEME_SET: Set<string>` + `isValid(input): boolean`, plus a companion type alias via `TConstValue<typeof ClassName>`.
421
+
422
+ **`Authorization`** - context keys.
423
+
424
+ | Constant | Value | Description |
425
+ |----------|-------|-------------|
426
+ | `Authorization.RULES` | `'authorization.rules'` | Context key for cached rules |
427
+ | `Authorization.SKIP_AUTHORIZATION` | `'authorization.skip'` | Context key to dynamically skip authorization |
428
+ | `Authorization.ENFORCER` | `'authorization.enforcer'` | Binding key prefix for enforcers |
429
+ | `Authorization.DOMAIN` | `'authorization.domain'` | Context key for the resolved request domain scope |
430
+
431
+ **`AuthorizationActions`** - `CREATE` `UPDATE` `DELETE` `EXECUTE` `READ` `WRITE` `MANAGE`. `AuthorizationActions.LATTICE` declares the standard action hierarchy consumed by `AuthorizationPolicyBuilder.actionLattice()`:
432
+
433
+ | `child` | `parent` |
434
+ |---|---|
435
+ | `READ`, `WRITE`, `EXECUTE` | `MANAGE` |
436
+ | `CREATE`, `UPDATE`, `DELETE` | `WRITE` |
437
+
438
+ **`AuthorizationDecisions`** - `ALLOW` `DENY` `ABSTAIN`.
439
+
440
+ | Method | String check | Number check |
441
+ |--------|-------------|--------------|
442
+ | `isAllow(input)` | `input.toLowerCase() === 'allow'` | `input > 0` |
443
+ | `isDeny(input)` | `input.toLowerCase() === 'deny'` | `input < 0` |
444
+ | `isAbstain(input)` | `input.toLowerCase() === 'abstain'` | `input === 0` |
445
+
446
+ **`AuthorizationEnforcerTypes`** - `CASBIN` (`'casbin'`), `CUSTOM` (`'custom'`).
447
+
448
+ **`CasbinEnforcerModelDrivers`** - `FILE` (`'file'`, load from a `.conf` path), `TEXT` (`'text'`, inline string).
449
+
450
+ **`CasbinEnforcerCachedDrivers`** - `REDIS` (`'redis'`) is the only driver; the in-memory driver was removed.
276
451
 
277
- | Registry | `getBindingPrefix()` returns |
278
- |----------|------------------------------|
279
- | `AuthenticationStrategyRegistry` | `Authentication.STRATEGY` |
280
- | `AuthorizationEnforcerRegistry` | `Authorization.ENFORCER` (`'authorization.enforcer'`) |
452
+ **`CasbinDomainMatchingFunctions`** - selectable for `ICasbinEnforcerOptions.domainMatching.fn`, each mapping 1:1 to a Casbin `Util.*Func`, applied to the **domain slot** of a role definition (e.g. `g`).
281
453
 
282
- ## Enforcer Registry
454
+ | Constant | Value | Description |
455
+ |----------|-------|-------------|
456
+ | `KEY_MATCH` | `'keyMatch'` | `*` is the only wildcard; exact compare otherwise (recommended for `Merchant_<uuid>`-style domains) |
457
+ | `KEY_MATCH_2` | `'keyMatch2'` | Adds URL-path `:param` segment matching |
458
+ | `KEY_MATCH_3` | `'keyMatch3'` | Adds `{param}` segment matching |
459
+ | `KEY_MATCH_4` | `'keyMatch4'` | `{param}` with repeated-name equality checks |
460
+ | `REGEX_MATCH` | `'regexMatch'` | Treats the stored/policy value as a full regular expression |
283
461
 
284
- <code v-pre>AuthorizationEnforcerRegistry</code> is a **singleton** that manages registered enforcers. It extends `AbstractAuthRegistry<IAuthorizationEnforcer>`.
462
+ > [!IMPORTANT]
463
+ > Applied as `fn(requestDomain, policyDomain)` - the wildcard must live on the **stored/policy** side. `keyMatch("Merchant_X", "*") === true`, `keyMatch("Merchant_X", "Merchant_X") === true`, `keyMatch("Merchant_X", "Merchant_Y") === false`.
285
464
 
286
- ### Class Hierarchy
465
+ **`CasbinRuleVariants`** - the Casbin line prefixes declared by the scoped model, numbered in request-tuple order (`sub -> dom -> obj -> act`).
287
466
 
467
+ | Constant | Value | Relation |
468
+ |----------|-------|----------|
469
+ | `P` | `'p'` | Permission policy line |
470
+ | `G` | `'g'` | Role membership + role inheritance (the `sub` axis) |
471
+ | `G2` | `'g2'` | User -> domain membership (the `dom` axis) |
472
+ | `G3` | `'g3'` | Domain hierarchy (the `dom` axis) |
473
+ | `G4` | `'g4'` | Resource hierarchy (the `obj` axis, via `objectMatch`) |
474
+ | `G5` | `'g5'` | Action hierarchy (the `act` axis) |
475
+
476
+ **`AuthorizationPolicyVariants`** - the DB `variant` discriminator stored on each `PolicyDefinition` row (the kind of "edge"). Each entry carries `action` (the DB value) and `rule` (the Casbin prefix `ScopedCasbinAdapter` emits for it).
477
+
478
+ | Variant | `action` (DB) | `rule` | Meaning |
479
+ |---------|---------------|--------|---------|
480
+ | `GRANT` | `'grant'` | `p` | Give a permission to a User or Role |
481
+ | `ASSIGN_ROLE` | `'assign_role'` | `g` | Give a User a Role (optionally domain-scoped) |
482
+ | `ROLE_INHERITS` | `'role_inherits'` | `g` | Role inherits another Role |
483
+ | `JOIN_DOMAIN` | `'join_domain'` | `g2` | User is a member of a Domain |
484
+ | `DOMAIN_INHERITS` | `'domain_inherits'` | `g3` | Domain nested under a parent Domain |
485
+ | `RESOURCE_INHERITS` | `'resource_inherits'` | `g4` | Resource nested under a broader Resource |
486
+ | `ACTION_INHERITS` | `'action_inherits'` | `g5` | Action implied by a broader Action |
487
+
488
+ `isValidAction(input)` / `isValidRule(input)` check membership; `ACTION_SCHEME_SET` / `RULE_SCHEME_SET` hold the sets.
489
+
490
+ **`AuthorizationDomainScopes`** - sentinel domain values on `grant` rows.
491
+
492
+ | Constant | Value | Meaning |
493
+ |----------|-------|---------|
494
+ | `ANY_MEMBER` | `'ANY_MEMBER'` | Applies in every domain the subject joined (checked via `g2`) |
495
+ | `SYSTEM_WIDE` | `'SYSTEM_WIDE'` | Applies system-wide, bypassing membership (super-admin) |
496
+
497
+ **`AuthorizationRoles`** - built-in role identifiers (see [AuthorizationRole](#authorizationrole)).
498
+
499
+ | Constant | Identifier | Priority |
500
+ |----------|------------|----------|
501
+ | `SUPER_ADMIN` | `'999_super-admin'` | 999 |
502
+ | `ADMIN` | `'900_admin'` | 900 |
503
+ | `USER` | `'010_user'` | 10 |
504
+ | `GUEST` | `'001_guest'` | 1 |
505
+ | `UNKNOWN_USER` | `'000_unknown-user'` | 0 |
506
+
507
+ Source -> [`common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/common/constants.ts)
508
+
509
+ ## CASBIN_RBAC_DOMAIN_SCOPED_MODEL
510
+
511
+ The exported `.conf` text for the scoped model - pass it to `ICasbinEnforcerOptions.model` with `driver: CasbinEnforcerModelDrivers.TEXT` and `isScoped: true`.
512
+
513
+ ```ini
514
+ [request_definition]
515
+ r = sub, dom, obj, act
516
+
517
+ [policy_definition]
518
+ p = sub, dom, obj, act, eft
519
+
520
+ [role_definition]
521
+ g = _, _, _
522
+ g2 = _, _
523
+ g3 = _, _
524
+ g4 = _, _
525
+ g5 = _, _
526
+
527
+ [policy_effect]
528
+ e = some(where (p.eft == allow)) && !some(where (p.eft == deny))
529
+
530
+ [matchers]
531
+ m = g(r.sub, p.sub, r.dom) && (p.dom == "SYSTEM_WIDE" || (p.dom == "ANY_MEMBER" && g2(r.sub, r.dom)) || g3(r.dom, p.dom)) && (objectMatch(r.obj, p.obj) || g4(r.obj, p.obj)) && g5(r.act, p.act)
288
532
  ```
289
- BaseHelper
290
- └── AbstractAuthRegistry<TItem>
291
- ├── AuthenticationStrategyRegistry (authenticate)
292
- └── AuthorizationEnforcerRegistry (authorize)
533
+
534
+ | Relation | Axis | Meaning |
535
+ |---|---|---|
536
+ | `g` | `sub` | `assign_role` (user -> role) + `role_inherits` (role -> role), domain-aware. Registered with `keyMatch` so a `*` domain on a link matches any request domain |
537
+ | `g2` | `dom` (membership) | `join_domain` - powers the `ANY_MEMBER` grant scope |
538
+ | `g3` | `dom` (nesting) | `domain_inherits`, plus a self-link so an exact domain always matches itself |
539
+ | `g4` | `obj` | `resource_inherits` - explicit non-standard nesting edges; registered with `objectMatch` |
540
+ | `g5` | `act` | `action_inherits`, plus a self-link |
541
+
542
+ **Effect** is casbin's `allow-and-deny` effector: a request needs a matching `allow` AND no matching `deny` - default-DENY, and an explicit `deny` always overrides an `allow`. This is deliberately NOT casbin's `deny-override` effector (`!some(where (p.eft == deny))`), which would be default-ALLOW.
543
+
544
+ **Domain clause**, matched by `p.dom`: `SYSTEM_WIDE` matches every domain (bypasses membership, super-admin); `ANY_MEMBER` matches every domain the subject joined (via `g2`); `<Type>_<id>` matches that domain, or a nested child via `g3`.
545
+
546
+ > [!NOTE]
547
+ > Relies on the default `DefaultRoleManager`'s self-link behavior (`hasLink(name, name) === true`) for `g3`/`g4`/`g5` - a custom role manager must preserve self-links.
548
+
549
+ Source -> [`enforcers/models/rbac-domain.model.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/enforcers/models/rbac-domain.model.ts)
550
+
551
+ ## AbstractAuthRegistry
552
+
553
+ Shared base for the authentication strategy registry and `AuthorizationEnforcerRegistry`. Provides descriptor storage, binding-key generation, and DI resolution.
554
+
555
+ ```typescript
556
+ abstract class AbstractAuthRegistry<TItem> extends BaseHelper {
557
+ protected descriptors: Map<string, TRegistryDescriptor<TItem>>;
558
+ constructor(opts: { scope: string });
559
+ protected abstract getBindingPrefix(): string;
560
+
561
+ getKey(opts: { name: string }): string; // `${prefix}.${name}`
562
+ getDefaultName(): string; // first registered descriptor (Map insertion order)
563
+ reset(): void; // clears the Map
564
+
565
+ protected registerDescriptor(opts: { container: Container; target: TClass<TItem>; name: string }): void;
566
+ protected resolveDescriptor(opts: { name: string }): TItem;
567
+ }
568
+
569
+ type TRegistryDescriptor<TItem> = { container: Container; targetClass: TClass<TItem> };
293
570
  ```
294
571
 
295
- ### Class
572
+ | Method | Description | Throws |
573
+ |--------|-------------|--------|
574
+ | `getKey({ name })` | Builds the binding key | `[getKey] Invalid name` if empty |
575
+ | `getDefaultName()` | First registered descriptor's name | `[ClassName] No items registered` if none |
576
+ | `registerDescriptor(opts)` | Stores the descriptor + binds the class `SINGLETON` in DI | - |
577
+ | `resolveDescriptor({ name })` | Resolves the instance from the DI container | `Descriptor not found: {name}` or `Failed to resolve: {name}` |
578
+ | `reset()` | Clears all descriptors | - |
579
+
580
+ `AuthorizationEnforcerRegistry.getBindingPrefix()` returns `Authorization.ENFORCER` (`'authorization.enforcer'`).
581
+
582
+ Source -> [`base/abstract-auth-registry.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/base/abstract-auth-registry.ts)
583
+
584
+ ## AuthorizationEnforcerRegistry
585
+
586
+ Singleton, extends `AbstractAuthRegistry<IAuthorizationEnforcer>`.
296
587
 
297
588
  ```typescript
298
589
  class AuthorizationEnforcerRegistry extends AbstractAuthRegistry<IAuthorizationEnforcer> {
@@ -300,199 +591,115 @@ class AuthorizationEnforcerRegistry extends AbstractAuthRegistry<IAuthorizationE
300
591
  private configuredEnforcers: Set<string>;
301
592
 
302
593
  static getInstance(): AuthorizationEnforcerRegistry;
303
- override reset(): void; // clears descriptors + configuredEnforcers
594
+ override reset(): void; // clears descriptors + configuredEnforcers
304
595
 
305
- protected getBindingPrefix(): string; // returns Authorization.ENFORCER
596
+ register(opts: {
597
+ container: Container;
598
+ enforcers: Array<
599
+ | { enforcer: TClass<IAuthorizationEnforcer>; name: string; type: 'casbin'; options?: ICasbinEnforcerOptions }
600
+ | { enforcer: TClass<IAuthorizationEnforcer>; name: string; type: 'custom'; options?: unknown }
601
+ >;
602
+ }): this;
306
603
 
307
- register(opts: { ... }): this;
308
604
  hasEnforcers(): boolean;
309
605
  getDefaultEnforcerName(): string;
310
606
  resolveEnforcer(opts: { name: string }): Promise<IAuthorizationEnforcer>;
311
607
  resolveOptions(): IAuthorizeOptions | undefined;
608
+ invalidateUserCache(opts: { user: IAuthorizationUser; enforcerName?: string }): Promise<{ invalidatedKeys: number }>;
609
+ rebuildUserCache(opts: { user; enforcerName? }): Promise<{ cacheKey: string; lineCount: number }>;
312
610
  }
313
611
  ```
314
612
 
315
- ### API
316
-
317
613
  | Method | Returns | Description |
318
614
  |--------|---------|-------------|
319
- | `getInstance()` | `AuthorizationEnforcerRegistry` | Returns the singleton instance (creates on first call) |
320
- | `register(opts)` | `this` | Registers enforcers with type-safe options. See below. |
321
- | `hasEnforcers()` | `boolean` | Returns `true` if any enforcers are registered (`descriptors.size > 0`). Used by the middleware to skip authorization when no enforcers exist. |
322
- | `getDefaultEnforcerName()` | `string` | Delegates to `getDefaultName()` -- returns the first registered enforcer's name |
323
- | `resolveEnforcer({ name })` | `Promise<IAuthorizationEnforcer>` | Resolves and auto-configures an enforcer (configure-once pattern via `configuredEnforcers` Set) |
615
+ | `getInstance()` | `AuthorizationEnforcerRegistry` | Singleton instance (created on first call) |
616
+ | `register(opts)` | `this` | Registers enforcers with type-safe options (chainable) |
617
+ | `hasEnforcers()` | `boolean` | `descriptors.size > 0` - used by the middleware to skip authorization when no enforcers exist |
618
+ | `getDefaultEnforcerName()` | `string` | Delegates to `getDefaultName()` |
619
+ | `resolveEnforcer({ name })` | `Promise<IAuthorizationEnforcer>` | Resolves + auto-configures once (`configuredEnforcers` Set) |
324
620
  | `resolveOptions()` | `IAuthorizeOptions \| undefined` | Iterates all registered containers looking for `AuthorizeBindingKeys.OPTIONS` |
325
- | `reset()` | `void` | Clears all descriptors AND the `configuredEnforcers` set |
326
-
327
- ### register()
328
-
329
- The `register` method accepts a discriminated union of enforcer descriptors:
330
-
331
- ```typescript
332
- register(opts: {
333
- container: Container;
334
- enforcers: Array<
335
- | {
336
- enforcer: TClass<IAuthorizationEnforcer>;
337
- name: string;
338
- type: 'casbin';
339
- options?: ICasbinEnforcerOptions;
340
- }
341
- | {
342
- enforcer: TClass<IAuthorizationEnforcer>;
343
- name: string;
344
- type: 'custom';
345
- options?: unknown;
346
- }
347
- >;
348
- }) => this
349
- ```
350
-
351
- **Behavior:**
352
- 1. Validates no duplicate names in the batch (across all `enforcers` in this call)
353
- 2. Validates each name is not already registered (against previously registered enforcers)
354
- 3. Calls `registerDescriptor()` -- binds each enforcer class as singleton: `authorization.enforcer.{name}`
355
- 4. If `options` is provided, binds it to `AuthorizeBindingKeys.enforcerOptions(name)` (`@app/authorize/enforcers/{name}/options`)
621
+ | `invalidateUserCache(opts)` | `Promise<{ invalidatedKeys }>` | Drops a user's cached policies - throws if the resolved enforcer lacks the optional method |
622
+ | `rebuildUserCache(opts)` | `Promise<{ cacheKey, lineCount }>` | Drops then immediately re-extracts + re-caches |
623
+ | `reset()` | `void` | Clears descriptors AND `configuredEnforcers` |
356
624
 
357
- > [!NOTE]
358
- > `register()` returns `this`, enabling method chaining. The `type` field provides TypeScript-level type safety for the `options` field -- `type: 'casbin'` constrains `options` to `ICasbinEnforcerOptions`, while `type: 'custom'` allows `unknown`.
625
+ **`register()` behavior:** validates no duplicate names within the call, validates each name is not already registered, binds each class as a singleton at `authorization.enforcer.{name}`, and - if `options` is given - binds it to `AuthorizeBindingKeys.enforcerOptions(name)`.
359
626
 
360
- ### Configure-Once Pattern
361
-
362
- The `resolveEnforcer()` method tracks which enforcers have been configured via the `configuredEnforcers: Set<string>`:
627
+ **Configure-once pattern:**
363
628
 
364
629
  ```typescript
365
630
  async resolveEnforcer(opts: { name: string }): Promise<IAuthorizationEnforcer> {
366
- const enforcer = this.resolveDescriptor(opts); // from AbstractAuthRegistry
367
-
631
+ const enforcer = this.resolveDescriptor(opts);
368
632
  if (!this.configuredEnforcers.has(opts.name)) {
369
633
  await enforcer.configure();
370
634
  this.configuredEnforcers.add(opts.name);
371
635
  }
372
-
373
636
  return enforcer;
374
637
  }
375
638
  ```
376
639
 
377
- First call: resolves + calls `configure()`. Subsequent calls: resolves only.
378
-
379
- ## IAuthorizationEnforcer Interface
640
+ Source -> [`enforcers/enforcer-registry.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/enforcers/enforcer-registry.ts)
380
641
 
381
- The core enforcer contract. All enforcers (Casbin, custom) must implement this interface.
642
+ ## IAuthorizationEnforcer interface
382
643
 
383
644
  ```typescript
384
645
  interface IAuthorizationEnforcer<
385
- E extends Env = Env,
386
- TAction = string,
387
- TResource = string,
388
- TRules = unknown,
389
- TBuildRulesReturn = ValueOrPromise<TRules>,
390
- TEvaluateReturn = ValueOrPromise<TAuthorizationDecision>,
646
+ E extends Env = Env, TAction = string, TResource = string, TRules = unknown,
647
+ TBuildRulesReturn = ValueOrPromise<TRules>, TEvaluateReturn = ValueOrPromise<TAuthorizationDecision>,
391
648
  > {
392
649
  name: string;
393
-
394
650
  configure(): ValueOrPromise<void>;
651
+ buildRules(opts: { user: IAuthorizationUser; context: TContext<E, string> }): TBuildRulesReturn;
652
+ evaluate(opts: { rules: TRules; request: IAuthorizationRequest<TAction, TResource>; context: TContext<E, string> }): TEvaluateReturn;
395
653
 
396
- buildRules(opts: {
397
- user: { principalType: string } & IAuthUser;
398
- context: TContext<E, string>;
399
- }): TBuildRulesReturn;
400
-
401
- evaluate(opts: {
402
- rules: TRules;
403
- request: IAuthorizationRequest<TAction, TResource>;
404
- context: TContext<E, string>;
405
- }): TEvaluateReturn;
654
+ /** Optional - implemented only by caching enforcers. */
655
+ invalidateUserCache?(opts: { user: IAuthorizationUser }): Promise<{ invalidatedKeys: number }>;
656
+ rebuildUserCache?(opts: { user: IAuthorizationUser }): Promise<{ cacheKey: string; lineCount: number }>;
406
657
  }
407
658
  ```
408
659
 
409
- ### Generic Parameters
410
-
411
- | Parameter | Default | Description |
412
- |-----------|---------|-------------|
660
+ | Generic | Default | Description |
661
+ |---------|---------|--------------|
413
662
  | `E` | `Env` | Hono `Env` type for typed context access |
414
- | `TAction` | `string` | Action type (string) |
415
- | `TResource` | `string` | Resource type (string) |
416
- | `TRules` | `unknown` | Rules type produced by `buildRules` and consumed by `evaluate` |
663
+ | `TAction` / `TResource` | `string` | Action / resource type |
664
+ | `TRules` | `unknown` | Rules type produced by `buildRules`, consumed by `evaluate` |
417
665
  | `TBuildRulesReturn` | `ValueOrPromise<TRules>` | Return type of `buildRules` |
418
666
  | `TEvaluateReturn` | `ValueOrPromise<TAuthorizationDecision>` | Return type of `evaluate` |
419
667
 
420
- ### TRules per Enforcer
421
-
422
- | Enforcer | TRules | Description |
423
- |----------|--------|-------------|
424
- | `CasbinAuthorizationEnforcer` | `ICasbinRules` | `{ user, lines }` - the user plus their resolved Casbin policy lines (loaded into a pooled enforcer at evaluate time) |
425
- | Custom | Any type | Your custom rules structure |
426
-
427
- ### Method Contracts
428
-
429
668
  | Method | Input | Returns | Called by |
430
- |--------|-------|---------|----------|
431
- | `configure()` | None | `void` | Registry on first `resolveEnforcer()` |
432
- | `buildRules` | `{ user, context }` | `TRules` | Provider at step 6 |
433
- | `evaluate` | `{ rules, request, context }` | `TAuthorizationDecision` | Provider at step 7 |
434
-
435
- ## IAuthorizationRequest Interface
436
-
437
- The request object passed to `evaluate()`:
669
+ |--------|-------|---------|-----------|
670
+ | `configure()` | - | `void` | Registry, on first `resolveEnforcer()` |
671
+ | `buildRules` | `{ user, context }` | `TRules` | Provider, pipeline step 6 |
672
+ | `evaluate` | `{ rules, request, context }` | `TAuthorizationDecision` | Provider, pipeline step 7 |
438
673
 
439
- ```typescript
440
- interface IAuthorizationRequest<TAction = string, TResource = string> {
441
- action: TAction;
442
- resource: TResource;
443
- conditions?: TAuthorizationConditions;
444
- /** Resolved domain scope: `"<DomainType>_<id>"` (e.g. `"Merchant_7"`) or the `"SYSTEM_WIDE"` sentinel. */
445
- domain?: string;
446
- }
447
- ```
448
-
449
- | Field | Type | Description |
450
- |-------|------|-------------|
451
- | `action` | `TAction` | Action being checked (e.g., `'read'`, `'create'`) |
452
- | `resource` | `TResource` | Resource being accessed (e.g., `'Article'`) |
453
- | `conditions` | `TAuthorizationConditions` | Optional key-value conditions for ABAC |
454
-
455
- ## Casbin Enforcer
674
+ `invalidateUserCache`/`rebuildUserCache` are feature-detected at runtime (`typeof enforcer.invalidateUserCache === 'function'`) - only `CasbinAuthorizationEnforcer` with a Redis cache implements them.
456
675
 
457
- `CasbinAuthorizationEnforcer` wraps the `casbin` library (optional peer dependency).
676
+ ## CasbinAuthorizationEnforcer
458
677
 
459
- ### Class
678
+ Wraps the `casbin` library (optional peer dependency). Each request evaluates on its **own** enforcer borrowed from a `BasePoolHelper<Enforcer>` - the adapter (DB load) only runs on a throwaway enforcer to build a user's lines (cached in Redis if configured); every request then enforces on a pooled enforcer freshly loaded with those lines. This isolates concurrency and keeps the DB out of the hot path.
460
679
 
461
680
  ```typescript
462
- class CasbinAuthorizationEnforcer<
463
- E extends Env = Env,
464
- TAction extends string = string,
465
- TResource extends string = string,
466
- >
681
+ class CasbinAuthorizationEnforcer<E extends Env = Env, TAction extends string = string, TResource extends string = string>
467
682
  extends BaseHelper
468
683
  implements IAuthorizationEnforcer<E, TAction, TResource, ICasbinRules>
469
684
  {
470
685
  name = 'CasbinAuthorizationEnforcer';
471
-
472
686
  private readonly MIN_EXPIRES_IN = 10_000;
473
- private pool: TNullable<BasePoolHelper<CasbinEnforcerType>>; // per-request enforcers
474
- private helper: TNullable<typeof CasbinHelper>; // casbin.Helper (loadPolicyLine)
687
+ private pool: TNullable<BasePoolHelper<CasbinEnforcerType>>;
688
+ private helper: TNullable<typeof CasbinHelper>; // casbin.Helper (loadPolicyLine)
475
689
  private readonly pendingLineFetches = new Map<string, Promise<string[]>>(); // single-flight
476
- private resolvedPayloadFn: TNullable<TNormalizePayloadFn>; // memoized in configure()
690
+ private resolvedPayloadFn: TNullable<TNormalizePayloadFn>; // memoized in configure()
477
691
 
478
- constructor(
479
- @inject({ key: AuthorizeBindingKeys.enforcerOptions('casbin') })
480
- private options: ICasbinEnforcerOptions<E, TAction, TResource>,
481
- );
692
+ constructor(@inject({ key: AuthorizeBindingKeys.enforcerOptions('casbin') }) private options: ICasbinEnforcerOptions<E, TAction, TResource>);
482
693
 
483
- // Lifecycle
484
694
  async configure(): Promise<void>;
485
695
  destroy(): void;
486
696
 
487
- // IAuthorizationEnforcer
488
- async buildRules(opts: { user; context }): Promise<ICasbinRules>; // { user, lines }
697
+ async buildRules(opts: { user; context }): Promise<ICasbinRules>; // { user, lines }
489
698
  async evaluate(opts: { rules; request; context }): Promise<TAuthorizationDecision>;
490
699
 
491
- // Optional cache management (Redis only)
492
700
  async invalidateUserCache(opts: { user }): Promise<{ invalidatedKeys: number }>;
493
701
  async rebuildUserCache(opts: { user }): Promise<{ cacheKey: string; lineCount: number }>;
494
702
 
495
- // Protected internals
496
703
  protected async registerMatchers(opts: { enforcer; casbin }): Promise<void>;
497
704
  protected assertMatcherCompilesSync(opts: { enforcer }): void;
498
705
  protected resolveModel(opts): Model;
@@ -505,205 +712,83 @@ class CasbinAuthorizationEnforcer<
505
712
  }
506
713
  ```
507
714
 
508
- > **Architecture in one line:** the adapter (DB load) runs only on a *throwaway* enforcer to build a
509
- > user's lines (cached in Redis); every request then enforces on a *pooled* enforcer freshly loaded
510
- > with those lines. This isolates concurrency and keeps the DB out of the hot path.
511
-
512
- ### Constructor
513
-
514
- Injects `ICasbinEnforcerOptions` from the DI container using the binding key `AuthorizeBindingKeys.enforcerOptions('casbin')`.
515
-
516
715
  ### configure()
517
716
 
518
- Called once by the registry on first use. Performs:
717
+ Called once by the registry on first use:
519
718
 
520
- 1. Dynamically imports `casbin` - throws `"casbin" is not installed` if missing.
521
- 2. Validates `options.model` - throws `options.model is required.` if missing.
522
- 3. Memoizes the payload normalizer (`options.normalizePayloadFn ?? defaultScopedPayloadFn()`).
719
+ 1. Dynamically imports `casbin` - throws if not installed.
720
+ 2. Validates `options.model` is present.
721
+ 3. Memoizes the payload normalizer (`options.normalizePayloadFn ?? defaultScopedPayloadFn()`; the latter is `undefined` unless `isScoped`).
523
722
  4. If `cached.use`, validates `expiresIn >= MIN_EXPIRES_IN` (10,000 ms).
524
- 5. Builds a **`BasePoolHelper<Enforcer>`** (`size = poolSize ?? 16`, `acquireTimeoutMs = poolAcquireTimeoutMs ?? 5000`). Each pooled enforcer is created **without an adapter** (`newEnforcer(model)` - no DB load at warmup), then `registerMatchers()` and `assertMatcherCompilesSync()` run on it.
723
+ 5. Builds a `BasePoolHelper<Enforcer>` (`size = poolSize ?? 16`, `acquireTimeoutMs = poolAcquireTimeoutMs ?? 5000`). Each pooled enforcer is created **without an adapter** (no DB load at warmup), then `registerMatchers()` and `assertMatcherCompilesSync()` run on it.
525
724
  6. `await pool.warmup()` - pre-creates the enforcers.
526
725
 
527
- `registerMatchers()` - when `isScoped`, registers `keyMatch` as the domain matching func on `g`, adds `objectMatch` as a function, and registers it as the matching func on the resource relation (`g4`). When `domainMatching` is set (non-scoped), registers the chosen `Util.*Func` on the named role definition. Always finishes with `buildRoleLinks()`.
528
-
529
- `assertMatcherCompilesSync()` - a boot-time smoke test: forces casbin's lazy matcher compile by running one dummy `enforceSync` (4 args when scoped/`normalizePayloadFn`, else 3), so a malformed matcher, an unregistered function, or an arity mismatch fails at warmup instead of on the first real request.
726
+ `registerMatchers()` - when `isScoped`, registers `keyMatch` as the domain matching func on `g`, adds `objectMatch` as a function, and registers it as the matching func on the resource relation (`g4`). When `domainMatching` is set (flat model), registers the chosen `Util.*Func` on the named role definition. Always finishes with `buildRoleLinks()`.
530
727
 
531
- ### destroy()
532
-
533
- `this.pool?.destroy()` - drains and disposes the pooled enforcers.
728
+ `assertMatcherCompilesSync()` - a boot-time smoke test: forces casbin's lazy matcher compile with one dummy `enforceSync` (4 args when scoped/`normalizePayloadFn`, else 3), so a malformed matcher, an unregistered function, or an arity mismatch fails at warmup instead of on the first real request.
534
729
 
535
730
  ### buildRules()
536
731
 
537
- Returns `ICasbinRules` = `{ user, lines }`. The `lines` are the user's complete Casbin policy lines.
538
-
539
- ```mermaid
540
- flowchart TD
541
- Start([buildRules]) --> Check{cached.use?}
542
- Check -->|false| Extract["extractUserLines(user)"]
543
- Check -->|true| Redis["fetchLinesWithRedisCache(user, cached)"]
544
- Redis --> Hit{Redis hit?}
545
- Hit -->|Yes| Lines([lines])
546
- Hit -->|No| SF["single-flight → extractUserLines + SET PX"]
547
- SF --> Lines
548
- Extract --> Lines
549
- Lines --> Return(["return { user, lines }"])
550
- ```
732
+ Returns `ICasbinRules = { user, lines }` - the user's complete Casbin policy lines.
551
733
 
552
- - **`extractUserLines(user)`** builds a fresh, **isolated** enforcer *with the adapter*, calls
553
- `adapter.loadFilteredPolicy({ principal: { type, id } })`, then `extractLinesFrom()` serializes every
554
- p-type and g-type rule back into lines. This throwaway enforcer never serves a request - that is the
555
- anti-poisoning guarantee.
556
- - **`fetchLinesWithRedisCache`** returns cached lines on hit (Redis owns expiry via `PX`). On miss it
557
- dedups concurrent misses through `pendingLineFetches` (single-flight), extracts once, and writes the
558
- lines back to Redis. A corrupt entry is logged and discarded (refetch), never a 500.
734
+ - `extractUserLines(user)` builds a fresh, isolated enforcer *with the adapter*, calls `adapter.loadFilteredPolicy({ principal: { type, id } })`, then `extractLinesFrom()` serializes every p-type and g-type rule (not just `p`/`g` - every `p*`/`g*` the model declares, so the scoped model's `g2`-`g5` hierarchies are included) back into lines.
735
+ - `fetchLinesWithRedisCache` returns cached lines on a hit (Redis owns expiry via `PX`). On a miss it dedups concurrent misses through `pendingLineFetches` (single-flight), extracts once, and writes the lines back to Redis. A corrupt entry is logged and discarded (refetch), never a 500.
559
736
 
560
737
  ### evaluate()
561
738
 
562
- Borrows an enforcer from the pool and evaluates **atomically** inside `pool.use`:
739
+ Borrows an enforcer from the pool and evaluates atomically inside `pool.use`:
563
740
 
564
- ```mermaid
565
- flowchart TD
566
- Start([evaluate]) --> Use["pool.use(enforcer =>"]
567
- Use --> Load["loadPolicyLinesIntoModel(enforcer, rules.lines)<br/>clearPolicy + loadPolicyLine* + buildRoleLinks"]
568
- Load --> Norm["normalizePayloadFn(user, action, resource, context)"]
569
- Norm --> Dom["domain = normalized.domain ?? request.domain ?? (isScoped ? SYSTEM_WIDE : undefined)"]
570
- Dom --> Enf["enforceWithExplain(vals)"]
571
- Enf --> Dec{allowed?}
572
- Dec -->|Yes| Allow([ALLOW])
573
- Dec -->|No| Deny([DENY])
574
- ```
741
+ 1. `loadPolicyLinesIntoModel(enforcer, rules.lines)` - `clearPolicy()` + `loadPolicyLine()` per line + `buildRoleLinks()`.
742
+ 2. `normalizePayloadFn(user, action, resource, context)` normalizes the payload.
743
+ 3. `domain = normalized.domain ?? request.domain ?? (isScoped ? SYSTEM_WIDE : undefined)`.
744
+ 4. `vals` is `[subject, domain, resource, action]` when a domain is present, else `[subject, resource, action]`.
745
+ 5. `enforceWithExplain(vals)` runs `enforceExSync` and logs the deciding policy on a DENY.
575
746
 
576
- - `vals` is `[subject, domain, resource, action]` when a domain is present (scoped), else `[subject, resource, action]`.
577
- - On any error inside `pool.use`, the pool **destroys** the borrowed enforcer (fail-closed); a fresh one is created on demand.
578
- - `enforceWithExplain` uses `enforceExSync` to also log the deciding policy on a DENY.
747
+ On any error inside `pool.use`, the pool **destroys** the borrowed enforcer (fail-closed); a fresh one is created on demand.
579
748
 
580
749
  ### invalidateUserCache() / rebuildUserCache()
581
750
 
582
- Redis-only (throw if caching is disabled). `invalidateUserCache` deletes the user's shared Redis key
583
- (next request rebuilds lazily). `rebuildUserCache` deletes then immediately re-extracts (on a throwaway
584
- enforcer) and re-caches. Because the key is shared in Redis, a single call is correct across instances.
585
-
586
- ### Protected Methods
587
-
588
- | Method | Output | Description |
589
- |--------|--------|-------------|
590
- | `registerMatchers` | `void` | Registers domain/resource matching funcs (+ `buildRoleLinks`); scoped vs `domainMatching` |
591
- | `assertMatcherCompilesSync` | `void` | Boot-time matcher smoke test (forces lazy compile) |
592
- | `resolveModel` | `Model` | Resolves casbin model from `file` or `text` driver |
593
- | `validateExpiresIn` | `void` | Throws if `expiresIn < MIN_EXPIRES_IN` |
594
- | `fetchLinesWithRedisCache` | `string[]` | Redis read → single-flight extract+write on miss |
595
- | `extractUserLines` | `string[]` | Throwaway enforcer + adapter `loadFilteredPolicy` → `extractLinesFrom` |
596
- | `extractLinesFrom` | `string[]` | Serializes every p-type and g-type rule into lines |
597
- | `loadPolicyLinesIntoModel` | `void` | `clearPolicy` + `loadPolicyLine` per line + `buildRoleLinks` |
598
- | `enforceWithExplain` | `boolean` | `enforceExSync`; logs the deciding rule on DENY |
599
-
600
- #### extractLinesFrom()
751
+ Redis-only (throw if caching is disabled). `invalidateUserCache` deletes the user's shared Redis key (next request rebuilds lazily). `rebuildUserCache` deletes then immediately re-extracts (on a throwaway enforcer) and re-caches. Because the key is shared in Redis, a single call is correct across instances.
601
752
 
602
- Serializes **all** policy + grouping rule types (not just `p`/`g`) so the cached payload is complete
603
- for the scoped model (`g2`…`g5`):
604
-
605
- ```typescript
606
- const model = enforcer.getModel();
607
- const lines: string[] = [];
608
-
609
- for (const ptype of model.model.get(CasbinRuleVariants.P)?.keys() ?? []) {
610
- for (const rule of await enforcer.getNamedPolicy(ptype)) lines.push([ptype, ...rule].join(', '));
611
- }
612
- for (const gtype of model.model.get(CasbinRuleVariants.G)?.keys() ?? []) {
613
- for (const rule of await enforcer.getNamedGroupingPolicy(gtype)) lines.push([gtype, ...rule].join(', '));
614
- }
615
- return lines;
616
- ```
617
-
618
- #### loadPolicyLinesIntoModel()
619
-
620
- Atomically resets a borrowed enforcer's model to exactly `lines`:
621
-
622
- ```typescript
623
- const model = opts.enforcer.getModel();
624
- model.clearPolicy();
625
- for (const line of opts.lines) {
626
- this.helper.loadPolicyLine(line, model);
627
- }
628
- await opts.enforcer.buildRoleLinks();
629
- ```
753
+ Source -> [`enforcers/casbin.enforcer.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/enforcers/casbin.enforcer.ts)
630
754
 
631
755
  ## BaseFilteredAdapter
632
756
 
633
- Thin read-only base for casbin `FilteredAdapter`s backed by a datasource. It owns the boilerplate
634
- every filtered adapter repeats - datasource/connector plumbing, the `isFiltered() === true` flag, the
635
- no-op write methods, and a `loadLines` helper. A subclass implements only `loadFilteredPolicy`: query
636
- the store for ONE principal's policies and turn them into casbin lines.
637
-
638
- ### Class
757
+ Thin read-only base for casbin `FilteredAdapter`s backed by a datasource. Owns the boilerplate every filtered adapter repeats; a subclass implements only `loadFilteredPolicy`.
639
758
 
640
759
  ```typescript
641
- abstract class BaseFilteredAdapter<TFilter = ICasbinPolicyFilter>
642
- extends BaseHelper
643
- implements FilteredAdapter
644
- {
760
+ abstract class BaseFilteredAdapter<TFilter = ICasbinPolicyFilter> extends BaseHelper implements FilteredAdapter {
645
761
  protected readonly dataSource: ICasbinPolicySource;
646
762
  protected get connector(): TCasbinPolicyConnector;
647
-
648
763
  constructor(opts: { scope: string; dataSource: ICasbinPolicySource });
649
764
 
650
- // Subclasses implement ONLY this:
651
765
  abstract loadFilteredPolicy(model: Model, filter: TFilter): Promise<void>;
652
-
653
766
  isFiltered(): boolean; // always true
654
767
 
655
- // No-op write methods (read-only adapter)
768
+ // Read-only adapter - no-op write methods
656
769
  async loadPolicy(): Promise<void>;
657
- async savePolicy(): Promise<boolean>; // returns true
770
+ async savePolicy(): Promise<boolean>; // returns true
658
771
  async addPolicy(): Promise<void>;
659
772
  async removePolicy(): Promise<void>;
660
773
  async removeFilteredPolicy(): Promise<void>;
661
774
 
662
- // Helper: parse + load casbin lines into a model.
775
+ protected async query<TRow>(opts: { statement: SQL }): Promise<TRow[]>;
663
776
  protected async loadLines(opts: { model: Model; lines: string[] }): Promise<void>;
664
777
  }
665
778
  ```
666
779
 
667
- ### Generic Parameters
668
-
669
- | Parameter | Default | Description |
670
- |-----------|---------|-------------|
671
- | `TFilter` | `ICasbinPolicyFilter` | Filter shape passed to `loadFilteredPolicy`. Subclasses may narrow it (e.g. `IScopedCasbinPolicyFilter`) |
672
-
673
- ### ICasbinPolicyFilter
674
-
675
- The default filter: which principal's policies to load. Subclasses may narrow it.
676
-
677
780
  ```typescript
678
- interface ICasbinPolicyFilter {
679
- principal: { type: string; id: IdType };
680
- }
681
- ```
682
-
683
- ### ICasbinPolicySource
781
+ interface ICasbinPolicyFilter { principal: { type: string; id: IdType }; }
684
782
 
685
- The minimal contract `BaseFilteredAdapter` depends on for its `dataSource` -- **not** the framework's
686
- general `IDataSource` interface. Any drizzle-backed datasource (e.g. `BasePostgresDataSource`) satisfies
687
- it structurally; the adapter only ever needs the connector to run policy queries.
688
-
689
- ```typescript
690
- interface ICasbinPolicySource {
691
- connector: TCasbinPolicyConnector;
692
- }
693
-
694
- type TCasbinPolicyConnector = ReturnType<
695
- typeof drizzle<Record<string, AnyType>, NodePgClient>
696
- >;
783
+ /** Minimal contract - NOT the framework's general IDataSource. Any Drizzle-backed datasource satisfies it. */
784
+ interface ICasbinPolicySource { connector: TCasbinPolicyConnector; }
785
+ type TCasbinPolicyConnector = PgDatabase<PgQueryResultHKT, Record<string, AnyType>>;
697
786
  ```
698
787
 
699
788
  > [!NOTE]
700
- > Components that only need query execution depend on this minimal local contract rather than a
701
- > connector class -- keeps the casbin adapters decoupled from the full datasource surface.
702
-
703
- ### loadLines()
789
+ > Components that only need query execution depend on this minimal local contract (`ICasbinPolicySource`) rather than a connector class - `src/components/**` never imports `@/connectors/postgres` for this.
704
790
 
705
- The base's only orchestration helper - subclasses call it from `loadFilteredPolicy` after assembling
706
- their casbin lines:
791
+ `query()` runs a raw `SQL` statement and normalizes the result to a row array - Drizzle's `execute()` shape differs per driver (node-postgres yields `{ rows }`, postgres-js yields the row list itself), so subclasses must call `query()` rather than read `.rows` directly. `loadLines()` is the other orchestration helper - subclasses call it after assembling their own casbin lines:
707
792
 
708
793
  ```typescript
709
794
  protected async loadLines(opts: { model: Model; lines: string[] }): Promise<void> {
@@ -714,42 +799,31 @@ protected async loadLines(opts: { model: Model; lines: string[] }): Promise<void
714
799
  }
715
800
  ```
716
801
 
717
- There are no template-method query hooks or shared line formatters on the base - a subclass owns its
718
- own queries and line construction (see `ScopedCasbinAdapter` below for the reference implementation).
802
+ Source -> [`adapters/base-filtered.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/adapters/base-filtered.ts), [`adapters/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/adapters/types.ts)
719
803
 
720
804
  ## ScopedCasbinAdapter
721
805
 
722
- The generic, read-only `FilteredAdapter` for the scoped RBAC model. It reads **one principal's edges**
723
- plus the **shared structural hierarchy** from a single `PolicyDefinition` edge table (joined to
724
- `Permission` for codes) and emits casbin lines. No subclassing - configure it with `IScopedCasbinEntities`.
725
-
726
- ### Class
806
+ The generic, read-only `FilteredAdapter` for the scoped RBAC model. Reads **one principal's edges** plus the **shared structural hierarchy** from a single `PolicyDefinition` table (joined to `Permission` for codes) and emits casbin lines. No subclassing - configure it with `IScopedCasbinEntities`.
727
807
 
728
808
  ```typescript
729
809
  class ScopedCasbinAdapter extends BaseFilteredAdapter<IScopedCasbinPolicyFilter> {
730
810
  protected readonly entities: IScopedCasbinEntities;
731
-
732
811
  constructor(opts: { dataSource: ICasbinPolicySource; entities: IScopedCasbinEntities });
733
812
 
734
813
  async loadFilteredPolicy(model: Model, filter: IScopedCasbinPolicyFilter): Promise<void>;
735
814
 
736
- // Per-principal queries
737
- protected queryRoleAssignments(opts): Promise<{ lines: string[]; roleIds: IdType[] }>; // g
738
- protected queryMemberships(opts): Promise<string[]>; // g2
739
- protected queryGrants(opts): Promise<string[]>; // → p
740
- // Shared hierarchy
741
- protected loadStructuralTrees(): Promise<string[]>; // role(g)/domain(g3)/resource(g4)/action(g5)
742
- protected queryRoleInherits(): Promise<string[]>; // g
743
- protected queryDomainInherits(): Promise<string[]>; // g3
744
- protected queryResourceInherits(): Promise<string[]>; // g4
745
- protected queryActionInherits(): Promise<string[]>; // → g5
746
- // Role closure (BFS over role_inherits)
747
- protected expandRoleClosure(opts: { role: { ids: IdType[]; edges: string[] } }): IdType[];
815
+ protected queryRoleAssignments(opts): Promise<{ lines: string[]; roleIds: IdType[] }>; // -> g
816
+ protected queryMemberships(opts): Promise<string[]>; // -> g2
817
+ protected queryGrants(opts): Promise<string[]>; // -> p
818
+ protected loadStructuralTrees(): Promise<string[]>; // role(g)/domain(g3)/resource(g4)/action(g5)
819
+ protected queryRoleInherits(): Promise<string[]>; // -> g
820
+ protected queryDomainInherits(): Promise<string[]>; // -> g3
821
+ protected queryResourceInherits(): Promise<string[]>; // -> g4
822
+ protected queryActionInherits(): Promise<string[]>; // -> g5
823
+ protected expandRoleClosure(opts: { role: { ids: IdType[]; edges: string[] } }): IdType[]; // BFS over role_inherits
748
824
  }
749
825
  ```
750
826
 
751
- ### IScopedCasbinEntities
752
-
753
827
  ```typescript
754
828
  interface IScopedCasbinTable { tableName: string; schemaName?: string; }
755
829
 
@@ -760,45 +834,18 @@ interface IScopedCasbinEntities {
760
834
  domainTypes: string[]; // e.g. ['Merchant', 'Organizer']
761
835
  softDelete?: { use: false } | { use: true; columnName: string };
762
836
  }
763
- ```
764
-
765
- ### IScopedCasbinPolicyFilter
766
837
 
767
- ```typescript
768
- interface IScopedCasbinPolicyFilter {
769
- principal: { type: string; id: IdType };
770
- }
838
+ interface IScopedCasbinPolicyFilter { principal: { type: string; id: IdType }; }
771
839
  ```
772
840
 
773
- ### loadFilteredPolicy() - two waves
841
+ **`loadFilteredPolicy()` - two waves:**
774
842
 
775
- ```mermaid
776
- flowchart TD
777
- Start([loadFilteredPolicy]) --> W1["Wave 1 (parallel): queryRoleAssignments (g) ·
778
- queryMemberships (g2) · queryGrants[user] (p) · loadStructuralTrees (g/g3/g4/g5)"]
779
- W1 --> Closure["expandRoleClosure(assigned roleIds, role_inherits edges)"]
780
- Closure --> W2["Wave 2: queryGrants[roleClosure] (p)"]
781
- W2 --> Load["loadLines(model, all lines)"]
782
- ```
783
-
784
- 1. **Wave 1 (parallel):** the principal's own edges - role assignments (`g`), domain memberships
785
- (`g2`), direct grants (`p`) - plus the shared structural trees (`role_inherits` → `g`,
786
- `domain_inherits` → `g3`, `resource_inherits` → `g4`, `action_inherits` → `g5`).
787
- 2. **Role closure:** `expandRoleClosure` does a cycle-safe BFS over the `role_inherits` (`g`) edges to
788
- collect the assigned roles + all transitive parents.
789
- 3. **Wave 2:** fetch the grants (`p`) of every role in the closure, so a user inherits the permissions
790
- of parent roles.
791
- 4. All lines are loaded via `loadLines`.
792
-
793
- ### SQL notes
843
+ 1. **Wave 1 (parallel):** the principal's own edges - role assignments (`g`), domain memberships (`g2`), direct grants (`p`) - plus the shared structural trees (`role_inherits` -> `g`, `domain_inherits` -> `g3`, `resource_inherits` -> `g4`, `action_inherits` -> `g5`).
844
+ 2. **Role closure:** `expandRoleClosure` does a cycle-safe BFS over the `role_inherits` (`g`) edges to collect the assigned roles plus all transitive parents.
845
+ 3. **Wave 2:** fetches the grants (`p`) of every role in the closure, so a user inherits parent-role permissions.
846
+ 4. All lines load via `loadLines`.
794
847
 
795
- All queries use the `sql` template tag from `drizzle-orm`. Tables are schema-qualified via
796
- `sql.identifier` (injection-safe); interpolated values (the `variant` discriminator from
797
- `AuthorizationPolicyVariants.*.action`, ids, types) are bound parameters. The soft-delete clause
798
- (`AND <alias>.<col> IS NULL`) is appended when `entities.softDelete.use` is true. `queryGrants`
799
- short-circuits to `[]` when given no subject ids (no DB round-trip).
800
-
801
- ### Usage Example
848
+ All queries use the `sql` template tag from `drizzle-orm`; tables are schema-qualified via `sql.identifier` (injection-safe), interpolated values are bound parameters. The soft-delete clause (`AND <alias>.<col> IS NULL`) is appended when `entities.softDelete.use` is true. `queryGrants` short-circuits to `[]` when given no subject ids (no DB round-trip).
802
849
 
803
850
  ```typescript
804
851
  import { ScopedCasbinAdapter } from '@venizia/ignis';
@@ -815,294 +862,349 @@ const adapter = new ScopedCasbinAdapter({
815
862
  });
816
863
  ```
817
864
 
818
- ## Authorization Provider
865
+ Source -> [`adapters/scoped-casbin.adapter.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/adapters/scoped-casbin.adapter.ts)
819
866
 
820
- `AuthorizationProvider` implements `IProvider<TAuthorizeFn>` and produces the middleware factory.
867
+ ## objectMatch
821
868
 
822
- ### Class
869
+ Resource-hierarchy matcher registered by the scoped model (both as a function `objectMatch(r.obj, p.obj)` in the matcher expression AND as the `g4` matching func). Decides whether a requested resource falls under a granted one, WITHOUT needing a stored edge for the "standard" case (dotted nesting is derived from the code itself):
823
870
 
824
871
  ```typescript
825
- class AuthorizationProvider extends BaseHelper implements IProvider<TAuthorizeFn> {
826
- constructor();
872
+ const objectMatch = (requested: string, granted: string): boolean => {
873
+ if (granted === '*') return true;
874
+ if (requested === granted) return true;
875
+ return requested.startsWith(`${granted}.`);
876
+ };
877
+ ```
827
878
 
828
- value(): TAuthorizeFn;
879
+ `objectMatch('Activation.findById', 'Activation')` -> `true` (dotted nesting - endpoint under subject). `objectMatch('OrderItem', 'Order')` -> `false` unless a `resource_inherits` (`g4`) edge links them - non-standard nesting always needs an explicit edge.
880
+
881
+ Source -> [`common/object-match.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/common/object-match.ts)
882
+
883
+ ## AuthorizationProvider
829
884
 
830
- private createAuthorizeMiddleware(opts: {
831
- spec: IAuthorizationSpec;
832
- enforcerName?: string;
833
- }): MiddlewareHandler;
885
+ Implements `IProvider<TAuthorizeFn>` and produces the middleware factory.
834
886
 
887
+ ```typescript
888
+ class AuthorizationProvider extends BaseHelper implements IProvider<TAuthorizeFn> {
889
+ constructor();
890
+ value(): TAuthorizeFn;
891
+ private createAuthorizeMiddleware(opts: { spec: IAuthorizationSpec; enforcerName?: string }): MiddlewareHandler;
835
892
  private extractUserRoles(opts: { user: IAuthUser }): string[];
836
893
  }
837
894
  ```
838
895
 
839
- ### Middleware Pipeline (7 Steps)
840
-
841
- The `createAuthorizeMiddleware` method creates a Hono middleware with this evaluation order:
896
+ **The 7-step pipeline** (`createAuthorizeMiddleware`):
842
897
 
843
898
  ```typescript
844
- // Step 1: Skip check
845
- const isSkipAuthorize = context.get(Authorization.SKIP_AUTHORIZATION);
846
- if (isSkipAuthorize) → next()
899
+ // 1. Skip check
900
+ if (context.get(Authorization.SKIP_AUTHORIZATION)) return next();
847
901
 
848
- // Step 2: User check
902
+ // 2. User check
849
903
  const user = context.get(Authentication.CURRENT_USER);
850
- if (!user) throw 401 "No authenticated user found"
904
+ if (!user) throw 401 'No authenticated user found';
851
905
 
852
- // Step 3: Role-based shortcuts (alwaysAllowRoles + allowedRoles merged)
853
- const needsRoleCheck = options?.alwaysAllowRoles?.length || spec.allowedRoles?.length;
906
+ // 3. Role shortcuts (alwaysAllowRoles + allowedRoles, merged; userRoles extracted once)
854
907
  if (needsRoleCheck) {
855
- const userRoles = extractUserRoles({ user }); // called once
856
- if (alwaysAllowRoles match) next() // logs "User has always-allow role"
857
- if (allowedRoles match) next() // logs "User has allowed role for route"
908
+ const userRoles = extractUserRoles({ user });
909
+ if (alwaysAllowRoles match) return next();
910
+ if (allowedRoles match) return next();
858
911
  }
859
912
 
860
- // Step 4: Voters (from IAuthorizationSpec)
861
- for (voter of spec.voters) {
862
- if (DENY) throw 403 "Authorization denied by voter"
863
- if (ALLOW) next()
864
- // ABSTAIN continue to next voter
913
+ // 4. Voters (from spec.voters)
914
+ for (const voter of spec.voters ?? []) {
915
+ const decision = await voter({ user, action, resource, context });
916
+ if (decision === DENY) throw 403 'Authorization denied by voter';
917
+ if (decision === ALLOW) return next();
918
+ // ABSTAIN -> next voter
865
919
  }
866
920
 
867
- // Step 5: Resolve enforcer (with no-enforcer fallback)
868
- if (!registry.hasEnforcers()) next() // skip if no enforcers registered
869
- const resolvedName = enforcerName ?? registry.getDefaultEnforcerName();
870
- const enforcer = await registry.resolveEnforcer({ name: resolvedName });
921
+ // 5. Resolve enforcer (no-enforcer fallback)
922
+ if (!registry.hasEnforcers()) return next();
923
+ const enforcer = await registry.resolveEnforcer({ name: enforcerName ?? registry.getDefaultEnforcerName() });
871
924
 
872
- // Step 5b: Resolve request domain scope (only when domain scoping is in play)
925
+ // 5b. Resolve request domain - only when spec.domain or a global domainResolver is in play
873
926
  if (spec.domain || options?.domainResolver) {
874
- const domainScope = await resolveRequestDomain({ spec, context, options }); // "<Type>_<id>" | SYSTEM_WIDE
875
- context.set(Authorization.DOMAIN, domainScope); // the enforcer reads this for request.domain
927
+ context.set(Authorization.DOMAIN, await resolveRequestDomain({ spec, context, options }));
876
928
  }
877
929
 
878
- // Step 6: Build/cache rules
930
+ // 6. Build/cache rules
879
931
  let rules = context.get(Authorization.RULES);
880
932
  if (!rules) {
881
- if (!user.principalType) throw 400 "principalType is required"
933
+ if (!user.principalType) throw 400 'user.principalType is required for enforcer-based authorization';
882
934
  rules = await enforcer.buildRules({ user, context });
883
- context.set(Authorization.RULES, rules); // cache on context
935
+ context.set(Authorization.RULES, rules);
884
936
  }
885
937
 
886
- // Step 7: Evaluate
887
- let decision = await enforcer.evaluate({ rules, request, context });
888
- if (decision === ABSTAIN) decision = options?.defaultDecision ?? DENY;
889
- if (decision !== ALLOW) throw 403 "Authorization denied"
938
+ // 7. Evaluate
939
+ let decision = await enforcer.evaluate({ rules, request: { action, resource, conditions, domain: context.get(Authorization.DOMAIN) }, context });
940
+ if (decision === ABSTAIN) decision = options?.defaultDecision ?? DENY;
941
+ if (decision !== ALLOW) throw 403 'Authorization denied';
890
942
 
891
- // All checks passed
892
943
  await next();
893
944
  ```
894
945
 
895
- ### Role Extraction
896
-
897
- The `extractUserRoles` method handles multiple role formats from the user object:
946
+ **`extractUserRoles()`** - normalizes `user.roles` to `string[]`, priority `identifier` > `name` > `String(id)`:
898
947
 
899
948
  ```typescript
900
- private extractUserRoles(opts: { user: IAuthUser }): string[] {
901
- const roles = user.roles; // via index signature
902
-
903
- if (!Array.isArray(roles)) {
904
- return [];
905
- }
906
-
907
- return roles.map((r: string | { identifier?: string; name?: string; id?: unknown }) => {
908
- if (typeof r === 'string') return r;
909
- return r.identifier ?? r.name ?? String(r.id ?? '');
910
- });
911
- }
949
+ roles.map(r => typeof r === 'string' ? r : (r.identifier ?? r.name ?? String(r.id ?? '')));
912
950
  ```
913
951
 
914
- **Extraction priority:** `identifier` > `name` > `String(id)`.
915
-
916
- Supports these formats:
917
- ```typescript
918
- // String array
919
- roles: ['admin', 'user']
920
-
921
- // Object array with identifier (preferred - matches AuthorizationRole.identifier)
922
- roles: [{ id: 1, identifier: '900_admin', priority: 900 }]
952
+ Source -> [`providers/authorization.provider.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/providers/authorization.provider.ts)
923
953
 
924
- // Object array with name fallback
925
- roles: [{ id: 1, name: 'admin' }]
926
-
927
- // Object array with id-only fallback
928
- roles: [{ id: 1 }]
929
- ```
930
-
931
- ## Standalone `authorize()` Function
954
+ ## Standalone authorize() function
932
955
 
933
956
  ```typescript
934
- // authorize.middleware.ts
935
957
  const authorizationProvider = new AuthorizationProvider();
936
958
  const authorizeFn = authorizationProvider.value();
937
959
 
938
- export const authorize = (opts: { spec: IAuthorizationSpec; enforcerName?: string }) => {
939
- return authorizeFn(opts);
940
- };
960
+ export const authorize = (opts: { spec: IAuthorizationSpec; enforcerName?: string }) => authorizeFn(opts);
941
961
  ```
942
962
 
943
- This is the primary export for creating authorization middleware. It creates a singleton `AuthorizationProvider` instance at module load time. The returned middleware handler is a standard Hono `MiddlewareHandler`.
963
+ A module-level singleton `AuthorizationProvider`; the returned handler is a standard Hono `MiddlewareHandler`.
944
964
 
945
- ## AuthorizationRole Model
965
+ Source -> [`middlewares/authorize.middleware.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/middlewares/authorize.middleware.ts)
946
966
 
947
- Value object representing a role with priority-based comparison.
967
+ ## AuthorizationRole
948
968
 
949
- ### Class
969
+ Value object for priority-based role comparison.
950
970
 
951
971
  ```typescript
952
972
  class AuthorizationRole implements IAuthorizationRole {
953
973
  readonly name: string;
954
974
  readonly priority: number;
955
- readonly delimiter: string; // default '_'
975
+ readonly delimiter: string; // default '_'
956
976
 
957
977
  static build(opts: { name: string; priority: number; delimiter?: string }): AuthorizationRole;
958
978
  constructor(opts: { name: string; priority: number; delimiter?: string });
959
979
 
960
- get identifier(): string;
980
+ get identifier(): string; // `${String(priority).padStart(3, '0')}${delimiter}${name}`
961
981
 
962
- compare(opts: { target: IAuthorizationRole }): number;
982
+ compare(opts: { target: IAuthorizationRole }): number; // this.priority - target.priority
963
983
  isHigherThan(opts: { target: IAuthorizationRole }): boolean;
964
984
  isLowerThan(opts: { target: IAuthorizationRole }): boolean;
965
985
  isEqualTo(opts: { target: IAuthorizationRole }): boolean;
966
986
  }
987
+
988
+ interface IAuthorizationRole { readonly name: string; readonly priority: number; readonly identifier: string; }
967
989
  ```
968
990
 
969
- ### IAuthorizationRole Interface
991
+ Source -> [`models/authorization-role.model.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/models/authorization-role.model.ts)
992
+
993
+ ## Policy and permission builders
994
+
995
+ Framework-owned row shapes for seeding a `PolicyDefinition`/`Permission` store that `ScopedCasbinAdapter` reads. Neither builder touches the database - both return plain objects for your own repository/insert calls.
996
+
997
+ ### AuthorizationPolicyBuilder
998
+
999
+ One static method per `PolicyDefinition` edge type (see [Authorization Policy Variants](#constants)). All accept a `TPolicyDomainInput` (a scope literal string or `{ type, id }`), serialized via `[type, id].join('_')`.
970
1000
 
971
1001
  ```typescript
972
- interface IAuthorizationRole {
973
- readonly name: string;
974
- readonly priority: number;
975
- readonly identifier: string;
1002
+ class AuthorizationPolicyBuilder {
1003
+ static readonly ACTION_PRINCIPAL = 'Action';
1004
+
1005
+ static grant(opts: { subject: { type; id }; permission: { type; id }; action: string; domain?: TNullable<TPolicyDomainInput>; effect: TAuthorizationDecision }): PolicyDefinitionRow;
1006
+ static assignRole(opts: { user: { type; id }; role: { type; id }; domain?: TNullable<TPolicyDomainInput> }): PolicyDefinitionRow;
1007
+ static joinDomain(opts: { user: { type; id }; domain: { type; id } }): PolicyDefinitionRow;
1008
+ static roleInherits(opts: { child: { type; id }; parent: { type; id } }): PolicyDefinitionRow;
1009
+ static resourceInherits(opts: { child: { type; id }; parent: { type; id } }): PolicyDefinitionRow; // Permission ids
1010
+ static actionInherits(opts: { child: TAuthorizationAction; parent: TAuthorizationAction }): PolicyDefinitionRow;
1011
+ static domainInherits(opts: { child: { type; id }; parent: { type; id } }): PolicyDefinitionRow;
1012
+
1013
+ /** All action_inherits rows for AuthorizationActions.LATTICE. Seed once, idempotently. */
1014
+ static actionLattice(): PolicyDefinitionRow[];
1015
+
1016
+ /** A role's coarse grant rows from resolved permission codes -> ids. */
1017
+ static roleGrants(opts: {
1018
+ role: { type; id };
1019
+ permission: { type: string; idByCode: ReadonlyMap<string, string> };
1020
+ grants: ReadonlyArray<{ resourceCode: string; action: string; domain?: TNullable<TPolicyDomainInput>; effect: TAuthorizationDecision }>;
1021
+ }): PolicyDefinitionRow[]; // unresolved resourceCodes are skipped
976
1022
  }
977
1023
  ```
978
1024
 
979
- ### Identifier Format
1025
+ `domain` defaults: `grant` -> `null` maps to `ANY_MEMBER` (the adapter's default); `assignRole` -> `null` maps to `*` (every domain).
1026
+
1027
+ ### AuthorizationPermissionBuilder
980
1028
 
981
- The identifier is generated as `{paddedPriority}{delimiter}{name}`. Priority is zero-padded to 3 digits:
1029
+ Builds `Permission` catalog rows (the `obj` axis the scoped matcher resolves). Generic over the name/description type, so i18n and plain-text apps both fit - the framework only owns the `code`/`method`/`action` shape.
982
1030
 
983
1031
  ```typescript
984
- // Priority 999, name 'super-admin', delimiter '_' → '999_super-admin'
985
- // Priority 10, name 'user', delimiter '_' '010_user'
986
- // Priority 1, name 'guest', delimiter '_' → '001_guest'
987
- // Priority 0, name 'unknown-user', delimiter '_' '000_unknown-user'
1032
+ class AuthorizationPermissionBuilder {
1033
+ static readonly RESOURCE_NODE_METHOD = '*'; // sentinel method for a coarse resource node
1034
+
1035
+ /** Standard repository method -> base action. Unlisted methods resolve to `execute`. */
1036
+ static readonly METHOD_ACTIONS: Record<string, TAuthorizationAction>; // find/findById/findOne/count -> read, create -> create, updateById/updateBy -> update, deleteById/deleteBy -> delete
1037
+ static readonly DEFAULT_CRUD_METHODS: string[]; // the methods `crud()` generates by default
1038
+
1039
+ static actionForMethod(method: string): TAuthorizationAction;
1040
+
1041
+ /** One operation-level permission, code = `<subject>.<method>`. */
1042
+ static operation<TName>(opts: { subject: string; method: string; scope: string; name: TName; description?: TNullable<TName>; action?: TAuthorizationAction; parentId?: TNullable<IdType> }): PermissionRow;
1043
+
1044
+ /** A coarse resource node (module or subject) used as a grant target, e.g. `Sale`. code has no dotted method; action defaults to `manage`. */
1045
+ static resourceNode<TName>(opts: { code: string; subject?: string; scope: string; name: TName; description?: TNullable<TName>; action?: TAuthorizationAction; parentId?: TNullable<IdType> }): PermissionRow;
1046
+
1047
+ /** The CRUD permission set for a subject (find/findById/findOne/count/create/updateById/updateBy/deleteById/deleteBy by default). */
1048
+ static crud<TName>(opts: {
1049
+ subject: string; scope: string;
1050
+ name: (ctx: { subject: string; method: string; action: TAuthorizationAction }) => TName;
1051
+ description?: (ctx) => TNullable<TName>;
1052
+ methods?: ReadonlyArray<string>;
1053
+ }): PermissionRow[];
1054
+ }
988
1055
  ```
989
1056
 
990
- Implementation: `[String(this.priority).padStart(3, '0'), this.name].join(this.delimiter)`
1057
+ Source -> [`common/policy-builder.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/common/policy-builder.ts), [`common/permission-builder.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/common/permission-builder.ts)
991
1058
 
992
- ### Comparison
1059
+ ## Model-based authorization metadata
993
1060
 
994
- Roles are compared by priority (higher number = higher privilege). `compare()` returns `this.priority - target.priority`:
1061
+ `@model({ settings: { authorize: { principal } } })` (see [Persistent Models](/guides/core-concepts/persistent/models#authorization-settings)) drives two things:
1062
+
1063
+ **1. `AUTHORIZATION_SUBJECT` static.** The `@model` decorator (`base/metadata/persistents.ts`) copies `settings.authorize.principal` onto the class as `AUTHORIZATION_SUBJECT`, unless the class already declares its own:
995
1064
 
996
1065
  ```typescript
997
- AuthorizationRoles.SUPER_ADMIN.isHigherThan({ target: AuthorizationRoles.ADMIN }); // true (999 > 900)
998
- AuthorizationRoles.GUEST.isLowerThan({ target: AuthorizationRoles.USER }); // true (1 < 10)
999
- AuthorizationRoles.ADMIN.isEqualTo({ target: AuthorizationRoles.ADMIN }); // true (900 === 900)
1066
+ const principal = metadata.settings?.authorize?.principal;
1067
+ if (principal && !Object.hasOwn(target, 'AUTHORIZATION_SUBJECT')) {
1068
+ target.AUTHORIZATION_SUBJECT = principal;
1069
+ }
1000
1070
  ```
1001
1071
 
1002
- ## Controller Integration
1003
-
1004
- ### How Authorization Middleware is Injected
1072
+ Declared on both entity bases: `BasePostgresEntity.AUTHORIZATION_SUBJECT?: string` and `BaseSearchEntity.AUTHORIZATION_SUBJECT?: string`.
1005
1073
 
1006
- Authorization is supported in both **REST** and **gRPC** controllers.
1074
+ **2. `IModelAuthorizeSettings`.**
1007
1075
 
1008
- #### REST Controllers
1076
+ ```typescript
1077
+ interface IModelAuthorizeSettings {
1078
+ principal: string;
1079
+ [extra: string | symbol]: any; // extensible - consumers can add extra authorization metadata
1080
+ }
1081
+ ```
1009
1082
 
1010
- The `AbstractRestController.buildRouteMiddlewares()` method handles middleware injection order. `getRouteConfigs()` calls `buildRouteMiddlewares()` internally:
1083
+ **3. `MetadataRegistry` queries** (mixed in by `ModelMetadataMixin`) - retrieve every model's authorization principal at runtime, e.g. to seed Casbin `Permission` rows:
1011
1084
 
1012
1085
  ```typescript
1013
- buildRouteMiddlewares<RouteConfig extends IAuthRouteConfig>(opts: { configs: RouteConfig }) {
1014
- const { authenticate = {}, authorize, ...restConfig } = configs;
1015
- const mws = [];
1086
+ getModelAuthorizeSettings(opts: { name: string }): IModelAuthorizeSettings | undefined;
1016
1087
 
1017
- // 1. Authenticate middleware (first)
1018
- if (strategies.length > 0) {
1019
- mws.push(authenticateFn({ strategies, mode }));
1020
- }
1088
+ getAuthorizeModelPrincipals(opts: { format: 'array' }): string[];
1089
+ getAuthorizeModelPrincipals(opts: { format: 'record' }): Record<string, string>; // modelName -> principal
1021
1090
 
1022
- // 2. Authorize middleware (second) - supports single or array
1023
- if (authorize) {
1024
- const specs = Array.isArray(authorize) ? authorize : [authorize];
1025
- for (const spec of specs) {
1026
- mws.push(authorizeFn({ spec }));
1027
- }
1028
- }
1091
+ getAuthorizeModelSettings(opts: { format: 'array' }): Array<{ name: string; authorize: IModelAuthorizeSettings; entry: IModelRegistryEntry }>;
1092
+ getAuthorizeModelSettings(opts: { format: 'record' }): Record<string, { authorize: IModelAuthorizeSettings; entry: IModelRegistryEntry }>;
1093
+ ```
1094
+
1095
+ Source -> [`base/metadata/persistents.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/metadata/persistents.ts), [`helpers/inversion/mixins/model.mixin.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/helpers/inversion/mixins/model.mixin.ts)
1029
1096
 
1030
- // 3. Custom middleware (last)
1031
- if (restConfig.middleware) { ... }
1097
+ ## Controller integration
1032
1098
 
1099
+ Authorization is supported in both **REST** and **gRPC** controllers, injected right after authentication.
1100
+
1101
+ ### REST controllers
1102
+
1103
+ `AbstractRestController.buildRouteMiddlewares()` builds the middleware array; `getRouteConfigs()` calls it internally.
1104
+
1105
+ ```typescript
1106
+ buildRouteMiddlewares<RouteConfig extends IAuthRouteConfig>(opts: { configs: RouteConfig }) {
1107
+ const { authenticate = {}, authorize, ...restConfig } = opts.configs;
1108
+ const mws = [];
1109
+ if (strategies.length > 0) mws.push(authenticateFn({ strategies, mode })); // 1. authenticate
1110
+ if (authorize) { // 2. authorize (single or array)
1111
+ for (const spec of Array.isArray(authorize) ? authorize : [authorize]) mws.push(authorizeFn({ spec }));
1112
+ }
1113
+ if (restConfig.middleware) { /* 3. custom middleware, last */ }
1033
1114
  return { restConfig, security, mws };
1034
1115
  }
1035
1116
  ```
1036
1117
 
1037
- #### gRPC Controllers
1118
+ ```typescript
1119
+ interface IAuthRouteConfig extends HonoRouteConfig {
1120
+ authenticate?: { strategies?: TAuthStrategy[]; mode?: TAuthMode };
1121
+ authorize?: IAuthorizationSpec | IAuthorizationSpec[];
1122
+ }
1123
+ ```
1124
+
1125
+ An array `authorize` creates one middleware per spec - all must pass.
1038
1126
 
1039
- The `AbstractGrpcController.buildRpcMiddlewares()` method provides symmetric middleware injection for gRPC:
1127
+ ### gRPC controllers
1128
+
1129
+ `AbstractGrpcController.buildRpcMiddlewares()` injects middleware in the same order, reading `authorize` from `IRpcMetadata`:
1040
1130
 
1041
1131
  ```typescript
1042
1132
  buildRpcMiddlewares(opts: { configs: IRpcMetadata }): TRpcMiddleware[] {
1043
- const { configs } = opts;
1044
1133
  const mws = [];
1045
-
1046
- // 1. Authenticate middleware (first)
1047
- if (configs.authenticate) { ... }
1048
-
1049
- // 2. Authorize middleware (second) - same pattern as REST
1050
- if (configs.authorize) {
1051
- const specs = Array.isArray(configs.authorize) ? configs.authorize : [configs.authorize];
1052
- for (const spec of specs) {
1134
+ if (configs.authenticate) { /* 1. authenticate */ }
1135
+ if (configs.authorize) { // 2. authorize
1136
+ for (const spec of Array.isArray(configs.authorize) ? configs.authorize : [configs.authorize]) {
1053
1137
  const authzMw = authorizeFn({ spec });
1054
1138
  mws.push((context, next) => authzMw(context, next));
1055
1139
  }
1056
1140
  }
1057
-
1058
1141
  return mws;
1059
1142
  }
1060
1143
  ```
1061
1144
 
1062
- ### IAuthRouteConfig
1063
-
1064
- Extended route config that supports both authentication and authorization (REST):
1065
-
1066
1145
  ```typescript
1067
- interface IAuthRouteConfig extends HonoRouteConfig {
1146
+ interface IRpcMetadata {
1147
+ name: string; // proto method name
1148
+ method: TGrpcMethod;
1068
1149
  authenticate?: { strategies?: TAuthStrategy[]; mode?: TAuthMode };
1069
1150
  authorize?: IAuthorizationSpec | IAuthorizationSpec[];
1070
1151
  }
1071
1152
  ```
1072
1153
 
1073
- When `authorize` is an array, each spec creates a separate middleware. All must pass for the handler to execute.
1154
+ ### CRUD factory authorization
1074
1155
 
1075
- ### Per-Route Auth Types (CRUD Factory)
1156
+ `defineControllerRouteConfigs` (`base/controllers/factory/definition.ts`) resolves each generated route's `authorize` via `resolveRouteAuthorize(routeKey)`:
1157
+
1158
+ 1. Endpoint `authenticate: { skip: true }` -> `undefined` (skips both authentication and authorization).
1159
+ 2. Endpoint `authorize: { skip: true }` -> `undefined` (authorization only; authentication still runs).
1160
+ 3. Endpoint `authorize` (single spec or array) -> used as-is.
1161
+ 4. No endpoint override -> falls back to the controller-level `authorize`.
1076
1162
 
1077
1163
  ```typescript
1078
- /** Per-route authorization config: { skip: true }, single spec, or array of specs. */
1079
1164
  type TRouteAuthorizeConfig = { skip: true } | IAuthorizationSpec | IAuthorizationSpec[];
1165
+ type TRouteAuthConfig = { authenticate?: TRouteAuthenticateConfig; authorize?: TRouteAuthorizeConfig };
1166
+ ```
1080
1167
 
1081
- /** Per-route auth config. Endpoint config takes precedence over controller-level config. */
1082
- type TRouteAuthConfig = {
1083
- authenticate?: TRouteAuthenticateConfig;
1084
- authorize?: TRouteAuthorizeConfig;
1085
- };
1168
+ Applied identically to `count`, `find`, `findById`, `findOne`, `create`, `updateById`, `updateBy`, `deleteById`, `deleteBy`.
1169
+
1170
+ Source -> [`base/controllers/rest/abstract.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/controllers/rest/abstract.ts), [`base/controllers/grpc/abstract.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/controllers/grpc/abstract.ts), [`base/controllers/factory/definition.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/controllers/factory/definition.ts)
1171
+
1172
+ ## Context variables
1173
+
1174
+ The auth module augments Hono's `ContextVariableMap` (`auth/context-variables.ts`), covering both authentication and authorization:
1175
+
1176
+ ```typescript
1177
+ declare module 'hono' {
1178
+ interface ContextVariableMap {
1179
+ [Authentication.CURRENT_USER]: IAuthUser;
1180
+ [Authentication.AUDIT_USER_ID]: IdType;
1181
+ [Authentication.SKIP_AUTHENTICATION]: boolean;
1182
+
1183
+ [Authorization.RULES]: unknown;
1184
+ [Authorization.SKIP_AUTHORIZATION]: boolean;
1185
+ [Authorization.DOMAIN]: string;
1186
+ }
1187
+ }
1086
1188
  ```
1087
1189
 
1088
- ## IAuthUser Interface
1190
+ | Key | Constant | Type | Description |
1191
+ |-----|----------|------|-------------|
1192
+ | `'authorization.rules'` | `Authorization.RULES` | `unknown` | Cached rules built by the enforcer - shape depends on the enforcer |
1193
+ | `'authorization.skip'` | `Authorization.SKIP_AUTHORIZATION` | `boolean` | Set `true` to dynamically skip authorization for this request |
1194
+ | `'authorization.domain'` | `Authorization.DOMAIN` | `string` | Resolved request domain scope (`"<Type>_<id>"` or `SYSTEM_WIDE`); read by the enforcer at step 7 |
1195
+ | `'authentication.currentUser'` | `Authentication.CURRENT_USER` | `IAuthUser` | Read at step 2 to get the authenticated user |
1196
+ | `'authentication.auditUserId'` | `Authentication.AUDIT_USER_ID` | `IdType` | Available for audit logging |
1089
1197
 
1090
- The user object available during authorization. Defined in `authenticate/common/types.ts`:
1198
+ ## IAuthUser / IJWTTokenPayload
1091
1199
 
1092
1200
  ```typescript
1093
1201
  interface IAuthUser {
1094
- userId: IdType; // IdType = number | string | bigint
1202
+ userId: IdType; // number | string | bigint
1095
1203
  [extra: string | symbol]: any;
1096
1204
  }
1097
1205
  ```
1098
1206
 
1099
- Key properties accessed by the authorization module via the index signature:
1100
- - `user.roles` -- used by `extractUserRoles()` for role-based shortcuts
1101
- - `user.principalType` -- required by `buildRules()` for enforcer-based evaluation
1102
-
1103
- ## IJWTTokenPayload Interface
1104
-
1105
- Full JWT token payload shape (extends `IAuthUser`):
1207
+ Accessed by the authorization module via the index signature: `user.roles` (role-based shortcuts), `user.principalType` (required for enforcer-based evaluation).
1106
1208
 
1107
1209
  ```typescript
1108
1210
  interface IJWTTokenPayload extends JWTPayload, IAuthUser {
@@ -1116,30 +1218,8 @@ interface IJWTTokenPayload extends JWTPayload, IAuthUser {
1116
1218
  }
1117
1219
  ```
1118
1220
 
1119
- ## Hono Context Variables (ContextVariableMap)
1120
-
1121
- The auth module augments Hono's `ContextVariableMap` in `auth/context-variables.ts`:
1122
-
1123
- ```typescript
1124
- declare module 'hono' {
1125
- interface ContextVariableMap {
1126
- // Authentication
1127
- [Authentication.CURRENT_USER]: IAuthUser; // 'authentication.currentUser'
1128
- [Authentication.AUDIT_USER_ID]: IdType; // 'authentication.auditUserId'
1129
- [Authentication.SKIP_AUTHENTICATION]: boolean; // 'authentication.skip'
1130
-
1131
- // Authorization
1132
- [Authorization.RULES]: unknown; // 'authorization.rules'
1133
- [Authorization.SKIP_AUTHORIZATION]: boolean; // 'authorization.skip'
1134
- [Authorization.DOMAIN]: string; // 'authorization.domain'
1135
- }
1136
- }
1137
- ```
1138
-
1139
- This enables type-safe `context.get()` and `context.set()` across all auth middleware.
1140
-
1141
- ## See Also
1221
+ ## See also
1142
1222
 
1143
- - [Setup & Configuration](./) -- Binding keys, options interfaces, and initial setup
1144
- - [Usage & Examples](./usage) -- Securing routes, voters, patterns, and CRUD integration
1145
- - [Error Reference](./errors) -- Error messages and troubleshooting
1223
+ - [Setup & Configuration](./) - binding keys, options interfaces, and initial setup
1224
+ - [Usage & Examples](./usage) - securing routes, voters, patterns, and CRUD integration
1225
+ - [Error Reference](./errors) - error messages and troubleshooting