@venizia/ignis-docs 0.0.6-2 → 0.0.7-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 (120) hide show
  1. package/README.md +125 -388
  2. package/dist/mcp-server/common/config.d.ts +0 -21
  3. package/dist/mcp-server/common/config.d.ts.map +1 -1
  4. package/dist/mcp-server/common/config.js +1 -36
  5. package/dist/mcp-server/common/config.js.map +1 -1
  6. package/dist/mcp-server/helpers/docs.helper.d.ts +0 -24
  7. package/dist/mcp-server/helpers/docs.helper.d.ts.map +1 -1
  8. package/dist/mcp-server/helpers/docs.helper.js +0 -25
  9. package/dist/mcp-server/helpers/docs.helper.js.map +1 -1
  10. package/dist/mcp-server/helpers/github.helper.d.ts +0 -13
  11. package/dist/mcp-server/helpers/github.helper.d.ts.map +1 -1
  12. package/dist/mcp-server/helpers/github.helper.js +3 -20
  13. package/dist/mcp-server/helpers/github.helper.js.map +1 -1
  14. package/dist/mcp-server/index.js +1 -20
  15. package/dist/mcp-server/index.js.map +1 -1
  16. package/dist/mcp-server/tools/base.tool.d.ts +4 -85
  17. package/dist/mcp-server/tools/base.tool.d.ts.map +1 -1
  18. package/dist/mcp-server/tools/base.tool.js +1 -38
  19. package/dist/mcp-server/tools/base.tool.js.map +1 -1
  20. package/dist/mcp-server/tools/docs/get-document-content.tool.d.ts +8 -2
  21. package/dist/mcp-server/tools/docs/get-document-content.tool.d.ts.map +1 -1
  22. package/dist/mcp-server/tools/docs/get-document-content.tool.js +1 -10
  23. package/dist/mcp-server/tools/docs/get-document-content.tool.js.map +1 -1
  24. package/dist/mcp-server/tools/docs/get-document-metadata.tool.d.ts +13 -2
  25. package/dist/mcp-server/tools/docs/get-document-metadata.tool.d.ts.map +1 -1
  26. package/dist/mcp-server/tools/docs/get-document-metadata.tool.js +1 -10
  27. package/dist/mcp-server/tools/docs/get-document-metadata.tool.js.map +1 -1
  28. package/dist/mcp-server/tools/docs/get-package-overview.tool.d.ts +16 -8
  29. package/dist/mcp-server/tools/docs/get-package-overview.tool.d.ts.map +1 -1
  30. package/dist/mcp-server/tools/docs/get-package-overview.tool.js +2 -25
  31. package/dist/mcp-server/tools/docs/get-package-overview.tool.js.map +1 -1
  32. package/dist/mcp-server/tools/docs/list-categories.tool.d.ts +5 -2
  33. package/dist/mcp-server/tools/docs/list-categories.tool.d.ts.map +1 -1
  34. package/dist/mcp-server/tools/docs/list-categories.tool.js +1 -10
  35. package/dist/mcp-server/tools/docs/list-categories.tool.js.map +1 -1
  36. package/dist/mcp-server/tools/docs/list-documents.tool.d.ts +11 -2
  37. package/dist/mcp-server/tools/docs/list-documents.tool.d.ts.map +1 -1
  38. package/dist/mcp-server/tools/docs/list-documents.tool.js +1 -10
  39. package/dist/mcp-server/tools/docs/list-documents.tool.js.map +1 -1
  40. package/dist/mcp-server/tools/docs/search-documents.tool.d.ts +13 -2
  41. package/dist/mcp-server/tools/docs/search-documents.tool.d.ts.map +1 -1
  42. package/dist/mcp-server/tools/docs/search-documents.tool.js +1 -10
  43. package/dist/mcp-server/tools/docs/search-documents.tool.js.map +1 -1
  44. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +9 -2
  45. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
  46. package/dist/mcp-server/tools/github/list-project-files.tool.js +1 -10
  47. package/dist/mcp-server/tools/github/list-project-files.tool.js.map +1 -1
  48. package/dist/mcp-server/tools/github/search-code.tool.d.ts +16 -2
  49. package/dist/mcp-server/tools/github/search-code.tool.d.ts.map +1 -1
  50. package/dist/mcp-server/tools/github/search-code.tool.js +2 -14
  51. package/dist/mcp-server/tools/github/search-code.tool.js.map +1 -1
  52. package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts +19 -6
  53. package/dist/mcp-server/tools/github/verify-dependencies.tool.d.ts.map +1 -1
  54. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +2 -19
  55. package/dist/mcp-server/tools/github/verify-dependencies.tool.js.map +1 -1
  56. package/dist/mcp-server/tools/github/view-source-file.tool.d.ts +8 -2
  57. package/dist/mcp-server/tools/github/view-source-file.tool.d.ts.map +1 -1
  58. package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -10
  59. package/dist/mcp-server/tools/github/view-source-file.tool.js.map +1 -1
  60. package/dist/mcp-server/tools/index.d.ts.map +1 -1
  61. package/dist/mcp-server/tools/index.js +0 -2
  62. package/dist/mcp-server/tools/index.js.map +1 -1
  63. package/package.json +68 -54
  64. package/wiki/best-practices/api-usage-examples.md +7 -5
  65. package/wiki/best-practices/code-style-standards/advanced-patterns.md +1 -1
  66. package/wiki/best-practices/code-style-standards/constants-configuration.md +1 -1
  67. package/wiki/best-practices/code-style-standards/control-flow.md +1 -1
  68. package/wiki/best-practices/code-style-standards/function-patterns.md +1 -1
  69. package/wiki/best-practices/common-pitfalls.md +1 -1
  70. package/wiki/best-practices/data-modeling.md +33 -1
  71. package/wiki/best-practices/error-handling.md +7 -4
  72. package/wiki/best-practices/performance-optimization.md +1 -1
  73. package/wiki/best-practices/security-guidelines.md +5 -4
  74. package/wiki/guides/core-concepts/components-guide.md +1 -1
  75. package/wiki/guides/core-concepts/controllers.md +14 -8
  76. package/wiki/guides/core-concepts/persistent/models.md +32 -0
  77. package/wiki/guides/core-concepts/services.md +2 -1
  78. package/wiki/guides/get-started/5-minute-quickstart.md +1 -1
  79. package/wiki/guides/reference/mcp-docs-server.md +0 -134
  80. package/wiki/guides/tutorials/building-a-crud-api.md +2 -1
  81. package/wiki/guides/tutorials/complete-installation.md +2 -2
  82. package/wiki/guides/tutorials/ecommerce-api.md +3 -3
  83. package/wiki/guides/tutorials/realtime-chat.md +7 -6
  84. package/wiki/index.md +2 -1
  85. package/wiki/references/base/components.md +2 -1
  86. package/wiki/references/base/controllers.md +19 -12
  87. package/wiki/references/base/middlewares.md +2 -1
  88. package/wiki/references/base/models.md +11 -2
  89. package/wiki/references/base/services.md +2 -1
  90. package/wiki/references/components/authentication/api.md +525 -205
  91. package/wiki/references/components/authentication/errors.md +502 -105
  92. package/wiki/references/components/authentication/index.md +388 -75
  93. package/wiki/references/components/authentication/usage.md +428 -266
  94. package/wiki/references/components/authorization/api.md +1213 -0
  95. package/wiki/references/components/authorization/errors.md +387 -0
  96. package/wiki/references/components/authorization/index.md +712 -0
  97. package/wiki/references/components/authorization/usage.md +758 -0
  98. package/wiki/references/components/health-check.md +2 -1
  99. package/wiki/references/components/index.md +2 -0
  100. package/wiki/references/components/socket-io/index.md +9 -4
  101. package/wiki/references/components/socket-io/usage.md +1 -1
  102. package/wiki/references/components/static-asset/index.md +3 -5
  103. package/wiki/references/components/swagger.md +2 -1
  104. package/wiki/references/configuration/environment-variables.md +2 -1
  105. package/wiki/references/configuration/index.md +2 -1
  106. package/wiki/references/helpers/error/index.md +1 -1
  107. package/wiki/references/helpers/index.md +1 -0
  108. package/wiki/references/helpers/inversion/index.md +1 -1
  109. package/wiki/references/helpers/kafka/index.md +305 -0
  110. package/wiki/references/helpers/redis/index.md +2 -9
  111. package/wiki/references/quick-reference.md +3 -5
  112. package/wiki/references/utilities/crypto.md +2 -2
  113. package/wiki/references/utilities/date.md +5 -5
  114. package/wiki/references/utilities/index.md +3 -11
  115. package/wiki/references/utilities/jsx.md +4 -2
  116. package/wiki/references/utilities/module.md +1 -1
  117. package/wiki/references/utilities/parse.md +4 -4
  118. package/wiki/references/utilities/performance.md +2 -2
  119. package/wiki/references/utilities/promise.md +4 -4
  120. package/wiki/references/utilities/request.md +2 -2
@@ -0,0 +1,1213 @@
1
+ # Authorization -- API Reference
2
+
3
+ > Architecture, enforcer internals, provider, registry, adapters, models, and middleware pipeline. See [Setup & Configuration](./) for initial setup.
4
+
5
+ ## Architecture
6
+
7
+ ### System Overview
8
+
9
+ ```mermaid
10
+ graph TB
11
+ subgraph Application["Application Setup"]
12
+ A1["1. bind IAuthorizeOptions"]
13
+ A2["2. this.component(AuthorizeComponent)"]
14
+ A3["3. AuthorizationEnforcerRegistry.register(...)"]
15
+ A1 --> A2 --> A3
16
+ end
17
+
18
+ A3 --> Registry["Enforcer Registry<br/>(singleton)"]
19
+ A3 --> Provider["AuthorizationProvider<br/>(IProvider)"]
20
+ A3 --> MW["authorize() Middleware"]
21
+
22
+ Registry --> Casbin["CasbinAuthorizationEnforcer<br/>+ FilteredAdapter"]
23
+ Registry --> Custom["Custom Enforcer"]
24
+
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
38
+ ```
39
+
40
+ ### Middleware Pipeline Flowchart
41
+
42
+ ```mermaid
43
+ flowchart TD
44
+ Start([Request]) --> Skip{SKIP_AUTHORIZATION?}
45
+ Skip -->|Yes| Next([next - skip all])
46
+ Skip -->|No| User{User on context?}
47
+ User -->|No| E401[/401 Unauthorized/]
48
+ User -->|Yes| Roles{Any role shortcuts?}
49
+ Roles -->|alwaysAllowRoles match| Next2([next - role bypass])
50
+ Roles -->|allowedRoles match| Next3([next - route role bypass])
51
+ Roles -->|No match| Voters{Has voters?}
52
+ Voters -->|DENY| E403a[/403 Denied by voter/]
53
+ Voters -->|ALLOW| Next4([next - voter allow])
54
+ Voters -->|ABSTAIN / none| Resolve[Resolve enforcer by name]
55
+ Resolve --> Cache{Rules cached?}
56
+ Cache -->|Yes| Evaluate
57
+ Cache -->|No| PType{principalType?}
58
+ PType -->|Missing| E400[/400 principalType required/]
59
+ PType -->|Present| Build[enforcer.buildRules]
60
+ Build --> CacheSet[Cache rules on context]
61
+ CacheSet --> Evaluate[enforcer.evaluate]
62
+ Evaluate --> Decision{Decision?}
63
+ Decision -->|ALLOW| Next5([next - authorized])
64
+ Decision -->|DENY| E403b[/403 Denied/]
65
+ Decision -->|ABSTAIN| Default{defaultDecision}
66
+ Default -->|ALLOW| Next5
67
+ Default -->|DENY| E403b
68
+ ```
69
+
70
+ ### Class Hierarchy
71
+
72
+ ```mermaid
73
+ classDiagram
74
+ class BaseHelper {
75
+ +logger
76
+ +scope
77
+ }
78
+
79
+ class AbstractAuthRegistry~TItem~ {
80
+ #descriptors: Map
81
+ #getBindingPrefix()* string
82
+ +getKey(opts) string
83
+ +getDefaultName() string
84
+ +reset() void
85
+ #registerDescriptor(opts) void
86
+ #resolveDescriptor(opts) TItem
87
+ }
88
+
89
+ class AuthorizationEnforcerRegistry {
90
+ -instance$ AuthorizationEnforcerRegistry
91
+ -configuredEnforcers: Set
92
+ +getInstance()$ AuthorizationEnforcerRegistry
93
+ +register(opts) this
94
+ +resolveEnforcer(opts) Promise
95
+ +resolveOptions() IAuthorizeOptions
96
+ }
97
+
98
+ class IAuthorizationEnforcer {
99
+ <<interface>>
100
+ +name: string
101
+ +configure() void
102
+ +buildRules(opts) TRules
103
+ +evaluate(opts) TAuthorizationDecision
104
+ }
105
+
106
+ class CasbinAuthorizationEnforcer {
107
+ -enforcer: CasbinEnforcer
108
+ -inMemoryInvalidationTimer
109
+ +configure() void
110
+ +destroy() void
111
+ +buildRules(opts) IAuthUser
112
+ +evaluate(opts) TAuthorizationDecision
113
+ }
114
+
115
+ class BaseFilteredAdapter~TEntities~ {
116
+ <<abstract>>
117
+ #entities: TEntities
118
+ +loadFilteredPolicy(model, filter) void
119
+ #buildDirectPolicies(opts)* string[]
120
+ #buildGroupPolicies(opts)* lines + roleIds
121
+ #buildRolePolicies(opts)* string[]
122
+ #formatDomain(domain) string
123
+ #toGroupLine(opts) string
124
+ #toPolicyLine(opts) string
125
+ }
126
+
127
+ class DrizzleCasbinAdapter {
128
+ -connector: TAnyConnector
129
+ #buildDirectPolicies(opts) string[]
130
+ #buildGroupPolicies(opts) lines + roleIds
131
+ #buildRolePolicies(opts) string[]
132
+ }
133
+
134
+ BaseHelper <|-- AbstractAuthRegistry
135
+ AbstractAuthRegistry <|-- AuthorizationEnforcerRegistry
136
+ IAuthorizationEnforcer <|.. CasbinAuthorizationEnforcer
137
+ BaseHelper <|-- CasbinAuthorizationEnforcer
138
+ BaseHelper <|-- BaseFilteredAdapter
139
+ BaseFilteredAdapter <|-- DrizzleCasbinAdapter
140
+ ```
141
+
142
+ ### Module File Layout
143
+
144
+ ```
145
+ auth/authorize/
146
+ ├── adapters/
147
+ │ ├── base-filtered.ts # BaseFilteredAdapter (abstract template)
148
+ │ └── drizzle-casbin.ts # DrizzleCasbinAdapter (concrete SQL)
149
+ ├── common/
150
+ │ ├── constants.ts # Authorization, AuthorizationActions, AuthorizationDecisions,
151
+ │ │ # AuthorizationRoles, AuthorizationEnforcerTypes,
152
+ │ │ # CasbinEnforcerCachedDrivers, CasbinEnforcerModelDrivers,
153
+ │ │ # CasbinRuleVariants
154
+ │ ├── keys.ts # AuthorizeBindingKeys
155
+ │ ├── types.ts # IAuthorizeOptions, IAuthorizationEnforcer,
156
+ │ │ # IAuthorizationSpec, ICasbinEnforcerOptions, etc.
157
+ │ └── index.ts # Barrel export
158
+ ├── enforcers/
159
+ │ ├── casbin.enforcer.ts # CasbinAuthorizationEnforcer
160
+ │ ├── enforcer-registry.ts # AuthorizationEnforcerRegistry (singleton)
161
+ │ └── index.ts # Barrel export
162
+ ├── middlewares/
163
+ │ └── authorize.middleware.ts # authorize() standalone function
164
+ ├── models/
165
+ │ ├── abilities/
166
+ │ │ ├── string-action.model.ts # StringAuthorizationAction
167
+ │ │ ├── string-resource.model.ts # StringAuthorizationResource
168
+ │ │ └── index.ts
169
+ │ ├── authorization-role.model.ts # AuthorizationRole
170
+ │ └── index.ts
171
+ ├── providers/
172
+ │ └── authorization.provider.ts # AuthorizationProvider
173
+ ├── component.ts # AuthorizeComponent
174
+ └── index.ts # Barrel export (all submodules)
175
+ ```
176
+
177
+ ### Tech Stack
178
+
179
+ | Technology | Purpose |
180
+ |------------|---------|
181
+ | **Hono middleware** | Route-level authorization via `createMiddleware` from `hono/factory` |
182
+ | **`casbin`** (optional) | External policy engine for Casbin enforcer. Peer dependency -- not bundled. |
183
+ | **`@venizia/ignis-helpers`** | `BaseHelper` base class, `getError` for error creation, `HTTP` result codes |
184
+ | **`@venizia/ignis-inversion`** | `IProvider` interface, `BindingScopes` for singleton registration |
185
+
186
+ ### Design Decisions
187
+
188
+ | Decision | Rationale |
189
+ |----------|-----------|
190
+ | **Enforcer-based** | Pluggable architecture -- swap between Casbin and custom enforcers without changing route configs |
191
+ | **Registry + co-located options** | Enforcer class, name, type, and options are registered together -- no split configuration across two binding sites |
192
+ | **Type-discriminated enforcers** | `type: 'casbin' \| 'custom'` in registry for type-safe options (`ICasbinEnforcerOptions` vs `unknown`) |
193
+ | **Voter pattern** | Custom logic that short-circuits before the enforcer (Spring Security inspiration) |
194
+ | **Rules caching** | Built rules cached on Hono context per-request -- avoids rebuilding for multi-spec routes |
195
+ | **Registry singleton** | Mirrors `AuthenticationStrategyRegistry` pattern -- consistent with the codebase |
196
+ | **Abstract base** | `AbstractAuthRegistry<T>` shared between authentication and authorization registries |
197
+ | **Filtered adapter pattern** | `BaseFilteredAdapter` template method pattern allows custom query backends while sharing formatting logic |
198
+ | **IAuthorizationComparable** | Generic comparison interface for custom action/resource types beyond plain strings |
199
+
200
+ ## Component Lifecycle
201
+
202
+ The `AuthorizeComponent` extends `BaseComponent` and executes during its `binding()` method:
203
+
204
+ | Step | Action | Failure |
205
+ |------|--------|---------|
206
+ | 1 | Resolve `IAuthorizeOptions` from container via `AuthorizeBindingKeys.OPTIONS` | Throws `[AuthorizeComponent] No authorize options found` |
207
+ | 2 | Call `bindAlwaysAllowRoles()` -- binds `alwaysAllowRoles` to `AuthorizeBindingKeys.ALWAYS_ALLOW_ROLES` if present | -- (skipped if no roles) |
208
+
209
+ ```typescript
210
+ class AuthorizeComponent extends BaseComponent {
211
+ constructor(
212
+ @inject({ key: CoreBindings.APPLICATION_INSTANCE }) private application: BaseApplication,
213
+ ) { ... }
214
+
215
+ override binding(): ValueOrPromise<void>;
216
+ private bindAlwaysAllowRoles(opts: { options: IAuthorizeOptions }): void;
217
+ }
218
+ ```
219
+
220
+ > [!NOTE]
221
+ > 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()`.
222
+
223
+ ## AbstractAuthRegistry
224
+
225
+ Shared base class for both authentication and authorization registries. Provides descriptor storage, binding key generation, and DI resolution.
226
+
227
+ ### TRegistryDescriptor
228
+
229
+ ```typescript
230
+ type TRegistryDescriptor<TItem> = {
231
+ container: Container;
232
+ targetClass: TClass<TItem>;
233
+ };
234
+ ```
235
+
236
+ ### Class
237
+
238
+ ```typescript
239
+ abstract class AbstractAuthRegistry<TItem> extends BaseHelper {
240
+ protected descriptors: Map<string, TRegistryDescriptor<TItem>>;
241
+
242
+ constructor(opts: { scope: string });
243
+
244
+ // Abstract -- subclass provides the binding key prefix
245
+ protected abstract getBindingPrefix(): string;
246
+
247
+ // Public API
248
+ getKey(opts: { name: string }): string;
249
+ getDefaultName(): string;
250
+ reset(): void;
251
+
252
+ // Protected internals
253
+ protected registerDescriptor(opts: { container: Container; target: TClass<TItem>; name: string }): void;
254
+ protected resolveDescriptor(opts: { name: string }): TItem;
255
+ }
256
+ ```
257
+
258
+ ### Methods
259
+
260
+ | Method | Description | Throws |
261
+ |--------|-------------|--------|
262
+ | `getKey({ name })` | Builds binding key as `{prefix}.{name}` | `[getKey] Invalid name` if name is empty |
263
+ | `getDefaultName()` | Returns the first registered descriptor's name (Map insertion order) | `[ClassName] No items registered` if none |
264
+ | `registerDescriptor(opts)` | Stores `TRegistryDescriptor` in Map + binds class as `SINGLETON` in DI container | -- |
265
+ | `resolveDescriptor({ name })` | Resolves instance from DI container by key | `Descriptor not found: {name}` or `Failed to resolve: {name}` |
266
+ | `reset()` | Clears all descriptors from the Map | -- |
267
+
268
+ ### Subclass Binding Prefixes
269
+
270
+ | Registry | `getBindingPrefix()` returns |
271
+ |----------|------------------------------|
272
+ | `AuthenticationStrategyRegistry` | `Authentication.STRATEGY` |
273
+ | `AuthorizationEnforcerRegistry` | `Authorization.ENFORCER` (`'authorization.enforcer'`) |
274
+
275
+ ## Enforcer Registry
276
+
277
+ <code v-pre>AuthorizationEnforcerRegistry</code> is a **singleton** that manages registered enforcers. It extends `AbstractAuthRegistry<IAuthorizationEnforcer>`.
278
+
279
+ ### Class Hierarchy
280
+
281
+ ```
282
+ BaseHelper
283
+ └── AbstractAuthRegistry<TItem>
284
+ ├── AuthenticationStrategyRegistry (authenticate)
285
+ └── AuthorizationEnforcerRegistry (authorize)
286
+ ```
287
+
288
+ ### Class
289
+
290
+ ```typescript
291
+ class AuthorizationEnforcerRegistry extends AbstractAuthRegistry<IAuthorizationEnforcer> {
292
+ private static instance: AuthorizationEnforcerRegistry;
293
+ private configuredEnforcers: Set<string>;
294
+
295
+ static getInstance(): AuthorizationEnforcerRegistry;
296
+ override reset(): void; // clears descriptors + configuredEnforcers
297
+
298
+ protected getBindingPrefix(): string; // returns Authorization.ENFORCER
299
+
300
+ register(opts: { ... }): this;
301
+ getDefaultEnforcerName(): string;
302
+ resolveEnforcer(opts: { name: string }): Promise<IAuthorizationEnforcer>;
303
+ resolveOptions(): IAuthorizeOptions | undefined;
304
+ }
305
+ ```
306
+
307
+ ### API
308
+
309
+ | Method | Returns | Description |
310
+ |--------|---------|-------------|
311
+ | `getInstance()` | `AuthorizationEnforcerRegistry` | Returns the singleton instance (creates on first call) |
312
+ | `register(opts)` | `this` | Registers enforcers with type-safe options. See below. |
313
+ | `getDefaultEnforcerName()` | `string` | Delegates to `getDefaultName()` -- returns the first registered enforcer's name |
314
+ | `resolveEnforcer({ name })` | `Promise<IAuthorizationEnforcer>` | Resolves and auto-configures an enforcer (configure-once pattern via `configuredEnforcers` Set) |
315
+ | `resolveOptions()` | `IAuthorizeOptions \| undefined` | Iterates all registered containers looking for `AuthorizeBindingKeys.OPTIONS` |
316
+ | `reset()` | `void` | Clears all descriptors AND the `configuredEnforcers` set |
317
+
318
+ ### register()
319
+
320
+ The `register` method accepts a discriminated union of enforcer descriptors:
321
+
322
+ ```typescript
323
+ register(opts: {
324
+ container: Container;
325
+ enforcers: Array<
326
+ | {
327
+ enforcer: TClass<IAuthorizationEnforcer>;
328
+ name: string;
329
+ type: 'casbin';
330
+ options?: ICasbinEnforcerOptions;
331
+ }
332
+ | {
333
+ enforcer: TClass<IAuthorizationEnforcer>;
334
+ name: string;
335
+ type: 'custom';
336
+ options?: unknown;
337
+ }
338
+ >;
339
+ }) => this
340
+ ```
341
+
342
+ **Behavior:**
343
+ 1. Validates no duplicate names in the batch (across all `enforcers` in this call)
344
+ 2. Validates each name is not already registered (against previously registered enforcers)
345
+ 3. Calls `registerDescriptor()` -- binds each enforcer class as singleton: `authorization.enforcer.{name}`
346
+ 4. If `options` is provided, binds it to `AuthorizeBindingKeys.enforcerOptions(name)` (`@app/authorize/enforcers/{name}/options`)
347
+
348
+ > [!NOTE]
349
+ > `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`.
350
+
351
+ ### Configure-Once Pattern
352
+
353
+ The `resolveEnforcer()` method tracks which enforcers have been configured via the `configuredEnforcers: Set<string>`:
354
+
355
+ ```typescript
356
+ async resolveEnforcer(opts: { name: string }): Promise<IAuthorizationEnforcer> {
357
+ const enforcer = this.resolveDescriptor(opts); // from AbstractAuthRegistry
358
+
359
+ if (!this.configuredEnforcers.has(opts.name)) {
360
+ await enforcer.configure();
361
+ this.configuredEnforcers.add(opts.name);
362
+ }
363
+
364
+ return enforcer;
365
+ }
366
+ ```
367
+
368
+ First call: resolves + calls `configure()`. Subsequent calls: resolves only.
369
+
370
+ ## IAuthorizationEnforcer Interface
371
+
372
+ The core enforcer contract. All enforcers (Casbin, custom) must implement this interface.
373
+
374
+ ```typescript
375
+ interface IAuthorizationEnforcer<
376
+ E extends Env = Env,
377
+ TAction = string,
378
+ TResource = string,
379
+ TRules = unknown,
380
+ TBuildRulesReturn = ValueOrPromise<TRules>,
381
+ TEvaluateReturn = ValueOrPromise<TAuthorizationDecision>,
382
+ > {
383
+ name: string;
384
+
385
+ configure(): ValueOrPromise<void>;
386
+
387
+ buildRules(opts: {
388
+ user: { principalType: string } & IAuthUser;
389
+ context: TContext<E, string>;
390
+ }): TBuildRulesReturn;
391
+
392
+ evaluate(opts: {
393
+ rules: TRules;
394
+ request: IAuthorizationRequest<TAction, TResource>;
395
+ context: TContext<E, string>;
396
+ }): TEvaluateReturn;
397
+ }
398
+ ```
399
+
400
+ ### Generic Parameters
401
+
402
+ | Parameter | Default | Description |
403
+ |-----------|---------|-------------|
404
+ | `E` | `Env` | Hono `Env` type for typed context access |
405
+ | `TAction` | `string` | Action type. Can be `string` or `IAuthorizationComparable` for custom comparison |
406
+ | `TResource` | `string` | Resource type. Can be `string` or `IAuthorizationComparable` for custom comparison |
407
+ | `TRules` | `unknown` | Rules type produced by `buildRules` and consumed by `evaluate` |
408
+ | `TBuildRulesReturn` | `ValueOrPromise<TRules>` | Return type of `buildRules` |
409
+ | `TEvaluateReturn` | `ValueOrPromise<TAuthorizationDecision>` | Return type of `evaluate` |
410
+
411
+ ### TRules per Enforcer
412
+
413
+ | Enforcer | TRules | Description |
414
+ |----------|--------|-------------|
415
+ | `CasbinAuthorizationEnforcer` | `IAuthUser` | User object (Casbin evaluates internally from loaded model) |
416
+ | Custom | Any type | Your custom rules structure |
417
+
418
+ ### Method Contracts
419
+
420
+ | Method | Input | Returns | Called by |
421
+ |--------|-------|---------|----------|
422
+ | `configure()` | None | `void` | Registry on first `resolveEnforcer()` |
423
+ | `buildRules` | `{ user, context }` | `TRules` | Provider at step 6 |
424
+ | `evaluate` | `{ rules, request, context }` | `TAuthorizationDecision` | Provider at step 7 |
425
+
426
+ ## IAuthorizationRequest Interface
427
+
428
+ The request object passed to `evaluate()`:
429
+
430
+ ```typescript
431
+ interface IAuthorizationRequest<TAction = string, TResource = string> {
432
+ action: TAction;
433
+ resource: TResource;
434
+ conditions?: TAuthorizationConditions;
435
+ }
436
+ ```
437
+
438
+ | Field | Type | Description |
439
+ |-------|------|-------------|
440
+ | `action` | `TAction` | Action being checked (e.g., `'read'`, `'create'`) |
441
+ | `resource` | `TResource` | Resource being accessed (e.g., `'Article'`) |
442
+ | `conditions` | `TAuthorizationConditions` | Optional key-value conditions for ABAC |
443
+
444
+ ## IAuthorizationComparable Interface
445
+
446
+ Generic comparison interface for custom action and resource types. Allows enforcers to work with objects that define their own comparison logic rather than plain strings.
447
+
448
+ ```typescript
449
+ interface IAuthorizationComparable<TElement = string, TCompareResult = number> {
450
+ value: TElement;
451
+ compare(other: TElement): TCompareResult;
452
+ isEqual(other: TElement): boolean;
453
+ }
454
+ ```
455
+
456
+ | Member | Type | Description |
457
+ |--------|------|-------------|
458
+ | `value` | `TElement` | The underlying value |
459
+ | `compare(other)` | `TCompareResult` | Compare with another value. Convention: `0` means equal. |
460
+ | `isEqual(other)` | `boolean` | Convenience check -- typically `compare(other) === 0` |
461
+
462
+ The `CasbinAuthorizationEnforcer` constrains its `TAction` and `TResource` generics to `string | IAuthorizationComparable`, allowing either plain strings or comparable objects.
463
+
464
+ ## StringAuthorizationAction
465
+
466
+ `IAuthorizationComparable` implementation for string-based actions with wildcard support.
467
+
468
+ ```typescript
469
+ class StringAuthorizationAction implements IAuthorizationComparable<string> {
470
+ static readonly WILDCARD = '*';
471
+
472
+ readonly value: string;
473
+
474
+ static build(opts: { value: string }): StringAuthorizationAction;
475
+ constructor(opts: { value: string });
476
+
477
+ compare(other: string): number;
478
+ isEqual(other: string): boolean;
479
+ }
480
+ ```
481
+
482
+ ### Comparison Logic
483
+
484
+ - If `this.value === '*'` (WILDCARD), `compare()` returns `0` (matches everything)
485
+ - Otherwise, `this.value.localeCompare(other)` is used
486
+
487
+ ```typescript
488
+ import { StringAuthorizationAction } from '@venizia/ignis';
489
+
490
+ const wildcard = StringAuthorizationAction.build({ value: '*' });
491
+ wildcard.isEqual('read'); // true — wildcard matches all
492
+ wildcard.isEqual('delete'); // true — wildcard matches all
493
+
494
+ const read = StringAuthorizationAction.build({ value: 'read' });
495
+ read.isEqual('read'); // true
496
+ read.isEqual('create'); // false
497
+ ```
498
+
499
+ ## StringAuthorizationResource
500
+
501
+ `IAuthorizationComparable` implementation for string-based resources using `localeCompare`.
502
+
503
+ ```typescript
504
+ class StringAuthorizationResource implements IAuthorizationComparable<string> {
505
+ readonly value: string;
506
+
507
+ static build(opts: { value: string }): StringAuthorizationResource;
508
+ constructor(opts: { value: string });
509
+
510
+ compare(other: string): number; // this.value.localeCompare(other)
511
+ isEqual(other: string): boolean; // compare(other) === 0
512
+ }
513
+ ```
514
+
515
+ Unlike `StringAuthorizationAction`, this class has no wildcard support -- comparison is always via `localeCompare`.
516
+
517
+ ```typescript
518
+ import { StringAuthorizationResource } from '@venizia/ignis';
519
+
520
+ const article = StringAuthorizationResource.build({ value: 'Article' });
521
+ article.isEqual('Article'); // true
522
+ article.isEqual('User'); // false
523
+ ```
524
+
525
+ ## Casbin Enforcer
526
+
527
+ `CasbinAuthorizationEnforcer` wraps the `casbin` library (optional peer dependency).
528
+
529
+ ### Class
530
+
531
+ ```typescript
532
+ class CasbinAuthorizationEnforcer<
533
+ E extends Env = Env,
534
+ TAction extends string | IAuthorizationComparable = string,
535
+ TResource extends string | IAuthorizationComparable = string,
536
+ >
537
+ extends BaseHelper
538
+ implements IAuthorizationEnforcer<E, TAction, TResource, IAuthUser>
539
+ {
540
+ name = 'CasbinAuthorizationEnforcer';
541
+
542
+ private readonly MIN_EXPIRES_IN = 10_000;
543
+ private enforcer: TNullable<CasbinEnforcerType | CasbinCachedEnforcerType>;
544
+ private inMemoryInvalidationTimer: TNullable<NodeJS.Timeout>;
545
+
546
+ constructor(
547
+ @inject({ key: AuthorizeBindingKeys.enforcerOptions('casbin') })
548
+ private options: ICasbinEnforcerOptions<E, TAction, TResource>,
549
+ );
550
+
551
+ // Lifecycle
552
+ async configure(): Promise<void>;
553
+ destroy(): void;
554
+
555
+ // IAuthorizationEnforcer
556
+ async buildRules(opts: { user; context }): Promise<IAuthUser>;
557
+ async evaluate(opts: { rules; request; context }): Promise<TAuthorizationDecision>;
558
+
559
+ // Protected internals
560
+ protected async resolveCasbinEnforcer(opts): Promise<CasbinEnforcerType | CasbinCachedEnforcerType>;
561
+ protected resolveModel(opts): Model;
562
+ protected validateExpiresIn(opts: { expiresIn: number }): void;
563
+ protected async loadPoliciesFromAdapter(opts): Promise<void>;
564
+ protected async loadPoliciesWithRedisCache(opts): Promise<void>;
565
+ protected async extractPolicyLines(): Promise<string[]>;
566
+ protected async loadPolicyLinesIntoModel(opts: { lines: string[] }): Promise<void>;
567
+ }
568
+ ```
569
+
570
+ ### Constructor
571
+
572
+ Injects `ICasbinEnforcerOptions` from the DI container using the binding key `AuthorizeBindingKeys.enforcerOptions('casbin')` (which resolves to `@app/authorize/enforcers/casbin/options`).
573
+
574
+ ### configure()
575
+
576
+ Called once by the registry on first use. Performs:
577
+
578
+ 1. Dynamically imports `casbin` -- throws `"casbin" is not installed` if missing
579
+ 2. Validates `options.model` -- throws `options.model is required.` if missing
580
+ 3. Resolves model via driver:
581
+ - `'file'` → `casbin.newModelFromFile(definition)`
582
+ - `'text'` → `casbin.newModelFromString(definition)`
583
+ 4. Creates enforcer based on cache config:
584
+ - `cached.use: false` → `casbin.newEnforcer(model, adapter)`
585
+ - `cached.driver: 'in-memory'` → `casbin.newCachedEnforcer(model, adapter)` + periodic invalidation timer (`setInterval`)
586
+ - `cached.driver: 'redis'` → `casbin.newEnforcer(model, adapter)` (Redis handles caching externally)
587
+ 5. Validates `expiresIn >= MIN_EXPIRES_IN` (10,000 ms) for both in-memory and redis cache drivers
588
+
589
+ ### destroy()
590
+
591
+ Cleans up the in-memory invalidation timer:
592
+
593
+ ```typescript
594
+ destroy() {
595
+ if (!this.inMemoryInvalidationTimer) {
596
+ return;
597
+ }
598
+ clearInterval(this.inMemoryInvalidationTimer);
599
+ this.inMemoryInvalidationTimer = null;
600
+ }
601
+ ```
602
+
603
+ Call this when shutting down the application to prevent timer leaks. Only relevant when using the `'in-memory'` cache driver.
604
+
605
+ ### buildRules()
606
+
607
+ Loads policies into the casbin enforcer model. Always uses `loadFilteredPolicy` -- the adapter must implement the `FilteredAdapter` interface.
608
+
609
+ ```mermaid
610
+ flowchart TD
611
+ Start([buildRules]) --> Check{cached.use?}
612
+ Check -->|false| Direct["loadPoliciesFromAdapter(user)"]
613
+ Check -->|true| Driver{cached.driver?}
614
+ Driver -->|in-memory| InMem["loadPoliciesFromAdapter(user)<br/>(CachedEnforcer handles invalidation)"]
615
+ Driver -->|redis| Redis["loadPoliciesWithRedisCache(user, cached)"]
616
+ Driver -->|other| Error[/500 Invalid cached.driver/]
617
+ Direct --> Return([return user])
618
+ InMem --> Return
619
+ Redis --> Return
620
+ ```
621
+
622
+ | Cache Driver | Behavior |
623
+ |-------------|----------|
624
+ | `use: false` | Load policies from adapter directly via `loadPoliciesFromAdapter()` |
625
+ | `'in-memory'` | Load policies from adapter (periodic invalidation handles cache refresh) |
626
+ | `'redis'` | Check Redis cache → hit: load lines into model via `loadPolicyLinesIntoModel()`; miss: load from adapter, extract lines via `extractPolicyLines()`, cache in Redis with TTL |
627
+
628
+ Returns the `IAuthUser` directly (Casbin evaluates policies from its internal model, not from the returned value).
629
+
630
+ ### evaluate()
631
+
632
+ Delegates to Casbin's synchronous `enforceSync()`:
633
+
634
+ ```typescript
635
+ // Without normalizePayloadFn:
636
+ // subject = `${user.principalType}_${user.userId}`
637
+ // enforceSync(subject, resource, action)
638
+
639
+ // With normalizePayloadFn:
640
+ // const { subject, domain, resource, action } = normalizePayloadFn({ user, ... })
641
+ // Domain-aware: enforceSync(subject, domain, resource, action)
642
+ // No domain: enforceSync(subject, resource, action)
643
+ ```
644
+
645
+ Returns `AuthorizationDecisions.ALLOW` or `AuthorizationDecisions.DENY`.
646
+
647
+ ### Protected Methods
648
+
649
+ | Method | Input | Output | Description |
650
+ |--------|-------|--------|-------------|
651
+ | `resolveCasbinEnforcer` | `{ casbin, model, adapter, cached }` | `Enforcer \| CachedEnforcer` | Creates the casbin enforcer instance based on cache config |
652
+ | `resolveModel` | `{ casbin, model }` | `Model` | Resolves casbin model from file or text via driver discriminant |
653
+ | `validateExpiresIn` | `{ expiresIn }` | `void` | Throws if `expiresIn < MIN_EXPIRES_IN` |
654
+ | `loadPoliciesFromAdapter` | `{ user }` | `void` | Calls `enforcer.loadFilteredPolicy({ principalType, principalValue })` |
655
+ | `loadPoliciesWithRedisCache` | `{ user, cached }` | `void` | Redis cache flow: check cache → hit: load lines; miss: load from adapter + cache |
656
+ | `extractPolicyLines` | -- | `string[]` | Extracts `p` and `g` lines from enforcer model via `getPolicy()` and `getGroupingPolicy()` |
657
+ | `loadPolicyLinesIntoModel` | `{ lines }` | `void` | Clears model, loads lines via `Helper.loadPolicyLine()`, rebuilds role links |
658
+
659
+ ### Policy Loading Internals
660
+
661
+ #### Redis Cache Flow
662
+
663
+ ```mermaid
664
+ flowchart TD
665
+ Start([buildRules with Redis]) --> Key["keyFn({ user }) → cacheKey"]
666
+ Key --> Valid{cacheKey truthy?}
667
+ Valid -->|No| E400[/400 Invalid cachedKey/]
668
+ Valid -->|Yes| Get["Redis GET cacheKey"]
669
+ Get --> Hit{Cache hit?}
670
+ Hit -->|Yes| Parse["JSON.parse(cachedData)"]
671
+ Parse --> LoadModel["loadPolicyLinesIntoModel(lines)"]
672
+ LoadModel --> LogHit["Log: Loaded CACHED Policies"]
673
+ Hit -->|No| Adapter["loadPoliciesFromAdapter(user)"]
674
+ Adapter --> Extract["extractPolicyLines()"]
675
+ Extract --> Set["Redis SET cacheKey, lines, PX expiresIn"]
676
+ Set --> LogMiss["Log: Loaded ADAPTER + CACHED Policies"]
677
+ LogHit --> Return([return user])
678
+ LogMiss --> Return
679
+ ```
680
+
681
+ #### extractPolicyLines()
682
+
683
+ Extracts all loaded policies from the enforcer model as casbin-format strings:
684
+
685
+ ```typescript
686
+ // Policy rules: ["p, user_123, Article, read, allow", ...]
687
+ const pRules = await this.enforcer.getPolicy();
688
+ const ps = pRules.map(r => [CasbinRuleVariants.P, ...r].join(', '));
689
+
690
+ // Group rules: ["g, user_123, role_admin, org_1", ...]
691
+ const gRules = await this.enforcer.getGroupingPolicy();
692
+ const gs = gRules.map(r => [CasbinRuleVariants.G, ...r].join(', '));
693
+
694
+ return [...ps, ...gs];
695
+ ```
696
+
697
+ #### loadPolicyLinesIntoModel()
698
+
699
+ Clears the model and reloads from cached string lines:
700
+
701
+ ```typescript
702
+ const { Helper } = await import('casbin');
703
+ const model = this.enforcer.getModel();
704
+ model.clearPolicy();
705
+
706
+ for (const line of opts.lines) {
707
+ Helper.loadPolicyLine(line, model);
708
+ }
709
+
710
+ await this.enforcer.buildRoleLinks();
711
+ ```
712
+
713
+ ## BaseFilteredAdapter
714
+
715
+ Abstract read-only casbin `FilteredAdapter` using a template method pattern. Subclasses provide query hooks; the base orchestrates loading and provides shared formatters.
716
+
717
+ ### Class
718
+
719
+ ```typescript
720
+ abstract class BaseFilteredAdapter<
721
+ TEntities extends IBaseFilteredAdapterEntities = IBaseFilteredAdapterEntities,
722
+ TFilter = ICasbinPolicyFilter,
723
+ TPolicyRow extends TBasePolicyRow = TBasePolicyRow,
724
+ >
725
+ extends BaseHelper
726
+ implements FilteredAdapter
727
+ {
728
+ protected readonly entities: TEntities;
729
+
730
+ constructor(opts: { scope: string; entities: TEntities });
731
+
732
+ // FilteredAdapter — public API
733
+ async loadPolicy(): Promise<void>; // no-op
734
+ async loadFilteredPolicy(model: Model, filter: TFilter): Promise<void>;
735
+ isFiltered(): boolean; // always true
736
+
737
+ // No-op write methods (read-only adapter)
738
+ async savePolicy(): Promise<boolean>; // returns true
739
+ async addPolicy(): Promise<void>; // no-op
740
+ async removePolicy(): Promise<void>; // no-op
741
+ async removeFilteredPolicy(): Promise<void>; // no-op
742
+
743
+ // Abstract hooks — subclasses provide the data queries
744
+ protected abstract buildDirectPolicies(opts): ValueOrPromise<string[]>;
745
+ protected abstract buildGroupPolicies(opts): ValueOrPromise<{ lines: string[]; roleIds }>;
746
+ protected abstract buildRolePolicies(opts): ValueOrPromise<string[]>;
747
+
748
+ // Shared formatters
749
+ protected formatDomain(domain: string | null): string | null;
750
+ protected toGroupLine(opts): string;
751
+ protected toPolicyLine(opts): string | null;
752
+ }
753
+ ```
754
+
755
+ ### Generic Parameters
756
+
757
+ | Parameter | Extends | Default | Description |
758
+ |-----------|---------|---------|-------------|
759
+ | `TEntities` | `IBaseFilteredAdapterEntities` | `IBaseFilteredAdapterEntities` | Entity configuration (subclass adds fields like `tableName`) |
760
+ | `TFilter` | -- | `ICasbinPolicyFilter` | Filter shape passed to `loadFilteredPolicy` |
761
+ | `TPolicyRow` | `TBasePolicyRow` | `TBasePolicyRow` | Policy row shape consumed by `toPolicyLine` |
762
+
763
+ ### IBaseFilteredAdapterEntities
764
+
765
+ ```typescript
766
+ interface IBaseFilteredAdapterEntities {
767
+ role: { principalType: string };
768
+ domain?: { principalType: string };
769
+ }
770
+ ```
771
+
772
+ ### ICasbinPolicyFilter
773
+
774
+ ```typescript
775
+ interface ICasbinPolicyFilter {
776
+ principalType: string;
777
+ principalValue: string | number;
778
+ }
779
+ ```
780
+
781
+ ### TBasePolicyRow
782
+
783
+ Declared as `type` (not `interface`) for Drizzle compatibility -- carries an implicit index signature required by `connector.execute<T>()`.
784
+
785
+ ```typescript
786
+ type TBasePolicyRow = {
787
+ variant: string; // 'policy' or 'group'
788
+ code: string; // permission/resource code
789
+ action: string | null;
790
+ subjectType: string;
791
+ subjectId: string | number;
792
+ effect: string | null;
793
+ domain: string | null;
794
+ };
795
+ ```
796
+
797
+ ### loadFilteredPolicy()
798
+
799
+ Orchestrates three query phases using `casbin.Helper.loadPolicyLine()`:
800
+
801
+ ```mermaid
802
+ flowchart LR
803
+ Start([loadFilteredPolicy]) --> D[buildDirectPolicies]
804
+ D -->|p lines| Load1[Load into model]
805
+ Load1 --> G[buildGroupPolicies]
806
+ G -->|g lines + roleIds| Load2[Load into model]
807
+ Load2 --> Check{roleIds empty?}
808
+ Check -->|No| R[buildRolePolicies]
809
+ R -->|p lines| Load3[Load into model]
810
+ Check -->|Yes| Done([Done])
811
+ Load3 --> Done
812
+ ```
813
+
814
+ ```
815
+ 1. buildDirectPolicies({ filter, rolePrincipal })
816
+ → Direct permissions assigned to the principal → casbin `p` lines
817
+
818
+ 2. buildGroupPolicies({ filter })
819
+ → Role assignments → casbin `g` lines + roleIds
820
+
821
+ 3. buildRolePolicies({ roleIds, rolePrincipal })
822
+ → Permissions inherited through roles → casbin `p` lines
823
+ (only if roleIds is non-empty)
824
+ ```
825
+
826
+ ### Abstract Query Hooks
827
+
828
+ | Hook | Input | Output | Description |
829
+ |------|-------|--------|-------------|
830
+ | `buildDirectPolicies` | `{ filter: TFilter, rolePrincipal: string }` | `string[]` | Direct permission `p` lines for the user |
831
+ | `buildGroupPolicies` | `{ filter: TFilter }` | `{ lines: string[], roleIds: (string \| number)[] }` | Role assignment `g` lines + role IDs |
832
+ | `buildRolePolicies` | `{ roleIds: (string \| number)[], rolePrincipal: string }` | `string[]` | Inherited permission `p` lines via roles |
833
+
834
+ ### Shared Formatters
835
+
836
+ | Method | Input | Output | Description |
837
+ |--------|-------|--------|-------------|
838
+ | `formatDomain(domain)` | `string \| null` | `string \| null` | Prepends `entities.domain.principalType` prefix if configured (e.g., `"Organization_<uuid>"`). Returns `null` if input is null. |
839
+ | `toGroupLine(opts)` | `{ subject, role, domain }` | `string` | Formats: <code v-pre>g, &lt;subject&gt;, &lt;role&gt;[, &lt;domain&gt;]</code> |
840
+ | `toPolicyLine(opts)` | `{ row: TPolicyRow }` | `string \| null` | Formats: <code v-pre>p, &lt;subject&gt;, [&lt;domain&gt;,] &lt;resource&gt;, &lt;action&gt;, &lt;effect&gt;</code>. Returns `null` if row has no action. Effect defaults to `'allow'`. |
841
+
842
+ ## DrizzleCasbinAdapter
843
+
844
+ Concrete read-only `FilteredAdapter` using raw SQL queries via Drizzle's `connector.execute()`.
845
+
846
+ ### Class
847
+
848
+ ```typescript
849
+ class DrizzleCasbinAdapter extends BaseFilteredAdapter<IDrizzleCasbinEntities> {
850
+ private connector: TAnyConnector;
851
+
852
+ constructor(opts: IDrizzleCasbinAdapterOptions);
853
+
854
+ protected async buildDirectPolicies(opts): Promise<string[]>;
855
+ protected async buildGroupPolicies(opts): Promise<{ lines; roleIds }>;
856
+ protected async buildRolePolicies(opts): Promise<string[]>;
857
+ }
858
+ ```
859
+
860
+ ### IDrizzleCasbinEntities
861
+
862
+ ```typescript
863
+ interface IDrizzleCasbinEntities extends IBaseFilteredAdapterEntities {
864
+ permission: { tableName: string; principalType: string };
865
+ role: { tableName: string; principalType: string };
866
+ policyDefinition: { tableName: string; principalType: string };
867
+ domain?: { principalType: string };
868
+ }
869
+ ```
870
+
871
+ ### IDrizzleCasbinAdapterOptions
872
+
873
+ ```typescript
874
+ interface IDrizzleCasbinAdapterOptions {
875
+ dataSource: IDataSource;
876
+ entities: IDrizzleCasbinEntities;
877
+ }
878
+ ```
879
+
880
+ ### SQL Queries
881
+
882
+ All queries use the `sql` template tag from `drizzle-orm` and filter by `variant` using `CasbinRuleVariants.POLICY` or `CasbinRuleVariants.GROUP` constants.
883
+
884
+ **buildDirectPolicies** -- direct permissions assigned to the user:
885
+ ```sql
886
+ SELECT pd.variant, p.code, pd.action,
887
+ pd.subject_type AS "subjectType", pd.subject_id AS "subjectId",
888
+ pd.effect, pd.domain
889
+ FROM {policyDefinition.tableName} pd
890
+ INNER JOIN {permission.tableName} p ON pd.target_id = p.id
891
+ WHERE pd.variant = 'policy'
892
+ AND pd.subject_type = :principalType
893
+ AND pd.subject_id = :principalValue
894
+ AND pd.target_type = :permission.principalType
895
+ ```
896
+
897
+ **buildGroupPolicies** -- role assignments for the user:
898
+ ```sql
899
+ SELECT pd.target_id AS "targetId", pd.domain
900
+ FROM {policyDefinition.tableName} pd
901
+ WHERE pd.variant = 'group'
902
+ AND pd.subject_type = :principalType
903
+ AND pd.subject_id = :principalValue
904
+ AND pd.target_type = :role.principalType
905
+ ```
906
+
907
+ **buildRolePolicies** -- permissions inherited through assigned roles:
908
+ ```sql
909
+ SELECT pd.variant, p.code, pd.action,
910
+ pd.subject_type AS "subjectType", pd.subject_id AS "subjectId",
911
+ pd.effect, pd.domain
912
+ FROM {policyDefinition.tableName} pd
913
+ INNER JOIN {permission.tableName} p ON pd.target_id = p.id
914
+ WHERE pd.variant = 'policy'
915
+ AND pd.subject_type = :role.principalType
916
+ AND pd.subject_id IN (:roleIds)
917
+ AND pd.target_type = :permission.principalType
918
+ ```
919
+
920
+ ### Usage Example
921
+
922
+ ```typescript
923
+ import { DrizzleCasbinAdapter } from '@venizia/ignis';
924
+
925
+ const adapter = new DrizzleCasbinAdapter({
926
+ dataSource: myPostgresDataSource,
927
+ entities: {
928
+ permission: { tableName: 'Permission', principalType: 'Permission' },
929
+ role: { tableName: 'Role', principalType: 'Role' },
930
+ policyDefinition: { tableName: 'PolicyDefinition', principalType: 'PolicyDefinition' },
931
+ domain: { principalType: 'Organization' },
932
+ },
933
+ });
934
+ ```
935
+
936
+ ## Authorization Provider
937
+
938
+ `AuthorizationProvider` implements `IProvider<TAuthorizeFn>` and produces the middleware factory.
939
+
940
+ ### Class
941
+
942
+ ```typescript
943
+ class AuthorizationProvider extends BaseHelper implements IProvider<TAuthorizeFn> {
944
+ constructor();
945
+
946
+ value(): TAuthorizeFn;
947
+
948
+ private createAuthorizeMiddleware(opts: {
949
+ spec: IAuthorizationSpec;
950
+ enforcerName?: string;
951
+ }): MiddlewareHandler;
952
+
953
+ private extractUserRoles(opts: { user: IAuthUser }): string[];
954
+ }
955
+ ```
956
+
957
+ ### Middleware Pipeline (7 Steps)
958
+
959
+ The `createAuthorizeMiddleware` method creates a Hono middleware with this evaluation order:
960
+
961
+ ```typescript
962
+ // Step 1: Skip check
963
+ const isSkipAuthorize = context.get(Authorization.SKIP_AUTHORIZATION);
964
+ if (isSkipAuthorize) → next()
965
+
966
+ // Step 2: User check
967
+ const user = context.get(Authentication.CURRENT_USER);
968
+ if (!user) → throw 401 "No authenticated user found"
969
+
970
+ // Step 3: Role-based shortcuts (alwaysAllowRoles + allowedRoles merged)
971
+ const needsRoleCheck = options?.alwaysAllowRoles?.length || spec.allowedRoles?.length;
972
+ if (needsRoleCheck) {
973
+ const userRoles = extractUserRoles({ user }); // called once
974
+ if (alwaysAllowRoles match) → next() // logs "User has always-allow role"
975
+ if (allowedRoles match) → next() // logs "User has allowed role for route"
976
+ }
977
+
978
+ // Step 4: Voters (from IAuthorizationSpec)
979
+ for (voter of spec.voters) {
980
+ if (DENY) → throw 403 "Authorization denied by voter"
981
+ if (ALLOW) → next()
982
+ // ABSTAIN → continue to next voter
983
+ }
984
+
985
+ // Step 5: Resolve enforcer
986
+ const resolvedName = enforcerName ?? registry.getDefaultEnforcerName();
987
+ const enforcer = await registry.resolveEnforcer({ name: resolvedName });
988
+
989
+ // Step 6: Build/cache rules
990
+ let rules = context.get(Authorization.RULES);
991
+ if (!rules) {
992
+ if (!user.principalType) → throw 400 "principalType is required"
993
+ rules = await enforcer.buildRules({ user, context });
994
+ context.set(Authorization.RULES, rules); // cache on context
995
+ }
996
+
997
+ // Step 7: Evaluate
998
+ let decision = await enforcer.evaluate({ rules, request, context });
999
+ if (decision === ABSTAIN) → decision = options?.defaultDecision ?? DENY;
1000
+ if (decision !== ALLOW) → throw 403 "Authorization denied"
1001
+
1002
+ // All checks passed
1003
+ await next();
1004
+ ```
1005
+
1006
+ ### Role Extraction
1007
+
1008
+ The `extractUserRoles` method handles multiple role formats from the user object:
1009
+
1010
+ ```typescript
1011
+ private extractUserRoles(opts: { user: IAuthUser }): string[] {
1012
+ const roles = user.roles; // via index signature
1013
+
1014
+ if (!Array.isArray(roles)) {
1015
+ return [];
1016
+ }
1017
+
1018
+ return roles.map((r: string | { identifier?: string; name?: string; id?: unknown }) => {
1019
+ if (typeof r === 'string') return r;
1020
+ return r.identifier ?? r.name ?? String(r.id ?? '');
1021
+ });
1022
+ }
1023
+ ```
1024
+
1025
+ **Extraction priority:** `identifier` > `name` > `String(id)`.
1026
+
1027
+ Supports these formats:
1028
+ ```typescript
1029
+ // String array
1030
+ roles: ['admin', 'user']
1031
+
1032
+ // Object array with identifier (preferred — matches AuthorizationRole.identifier)
1033
+ roles: [{ id: 1, identifier: '900_admin', priority: 900 }]
1034
+
1035
+ // Object array with name fallback
1036
+ roles: [{ id: 1, name: 'admin' }]
1037
+
1038
+ // Object array with id-only fallback
1039
+ roles: [{ id: 1 }]
1040
+ ```
1041
+
1042
+ ## Standalone `authorize()` Function
1043
+
1044
+ ```typescript
1045
+ // authorize.middleware.ts
1046
+ const authorizationProvider = new AuthorizationProvider();
1047
+ const authorizeFn = authorizationProvider.value();
1048
+
1049
+ export const authorize = (opts: { spec: IAuthorizationSpec; enforcerName?: string }) => {
1050
+ return authorizeFn(opts);
1051
+ };
1052
+ ```
1053
+
1054
+ 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`.
1055
+
1056
+ ## AuthorizationRole Model
1057
+
1058
+ Value object representing a role with priority-based comparison.
1059
+
1060
+ ### Class
1061
+
1062
+ ```typescript
1063
+ class AuthorizationRole implements IAuthorizationRole {
1064
+ readonly name: string;
1065
+ readonly priority: number;
1066
+ readonly delimiter: string; // default '_'
1067
+
1068
+ static build(opts: { name: string; priority: number; delimiter?: string }): AuthorizationRole;
1069
+ constructor(opts: { name: string; priority: number; delimiter?: string });
1070
+
1071
+ get identifier(): string;
1072
+
1073
+ compare(opts: { target: IAuthorizationRole }): number;
1074
+ isHigherThan(opts: { target: IAuthorizationRole }): boolean;
1075
+ isLowerThan(opts: { target: IAuthorizationRole }): boolean;
1076
+ isEqualTo(opts: { target: IAuthorizationRole }): boolean;
1077
+ }
1078
+ ```
1079
+
1080
+ ### IAuthorizationRole Interface
1081
+
1082
+ ```typescript
1083
+ interface IAuthorizationRole {
1084
+ readonly name: string;
1085
+ readonly priority: number;
1086
+ readonly identifier: string;
1087
+ }
1088
+ ```
1089
+
1090
+ ### Identifier Format
1091
+
1092
+ The identifier is generated as `{paddedPriority}{delimiter}{name}`. Priority is zero-padded to 3 digits:
1093
+
1094
+ ```typescript
1095
+ // Priority 999, name 'super-admin', delimiter '_' → '999_super-admin'
1096
+ // Priority 10, name 'user', delimiter '_' → '010_user'
1097
+ // Priority 1, name 'guest', delimiter '_' → '001_guest'
1098
+ // Priority 0, name 'unknown-user', delimiter '_' → '000_unknown-user'
1099
+ ```
1100
+
1101
+ Implementation: `[String(this.priority).padStart(3, '0'), this.name].join(this.delimiter)`
1102
+
1103
+ ### Comparison
1104
+
1105
+ Roles are compared by priority (higher number = higher privilege). `compare()` returns `this.priority - target.priority`:
1106
+
1107
+ ```typescript
1108
+ AuthorizationRoles.SUPER_ADMIN.isHigherThan({ target: AuthorizationRoles.ADMIN }); // true (999 > 900)
1109
+ AuthorizationRoles.GUEST.isLowerThan({ target: AuthorizationRoles.USER }); // true (1 < 10)
1110
+ AuthorizationRoles.ADMIN.isEqualTo({ target: AuthorizationRoles.ADMIN }); // true (900 === 900)
1111
+ ```
1112
+
1113
+ ## Controller Integration
1114
+
1115
+ ### How Authorization Middleware is Injected
1116
+
1117
+ The `AbstractController.getRouteConfigs()` method handles middleware injection order:
1118
+
1119
+ ```typescript
1120
+ getRouteConfigs<RouteConfig extends IAuthRouteConfig>(opts: { configs: RouteConfig }) {
1121
+ const { authenticate = {}, authorize, ...restConfig } = configs;
1122
+ const mws = [];
1123
+
1124
+ // 1. Authenticate middleware (first)
1125
+ if (strategies.length > 0) {
1126
+ mws.push(authenticateFn({ strategies, mode }));
1127
+ }
1128
+
1129
+ // 2. Authorize middleware (second) — supports single or array
1130
+ if (authorize) {
1131
+ const specs = Array.isArray(authorize) ? authorize : [authorize];
1132
+ for (const spec of specs) {
1133
+ mws.push(authorizeFn({ spec }));
1134
+ }
1135
+ }
1136
+
1137
+ // 3. Custom middleware (last)
1138
+ if (restConfig.middleware) { ... }
1139
+
1140
+ return createRoute({ ...restConfig, middleware: mws, ... });
1141
+ }
1142
+ ```
1143
+
1144
+ ### IAuthRouteConfig
1145
+
1146
+ Extended route config that supports both authentication and authorization:
1147
+
1148
+ ```typescript
1149
+ interface IAuthRouteConfig extends HonoRouteConfig {
1150
+ authenticate?: { strategies?: TAuthStrategy[]; mode?: TAuthMode };
1151
+ authorize?: IAuthorizationSpec | IAuthorizationSpec[];
1152
+ }
1153
+ ```
1154
+
1155
+ When `authorize` is an array, each spec creates a separate middleware. All must pass for the handler to execute.
1156
+
1157
+ ## IAuthUser Interface
1158
+
1159
+ The user object available during authorization. Defined in `authenticate/common/types.ts`:
1160
+
1161
+ ```typescript
1162
+ interface IAuthUser {
1163
+ userId: IdType; // IdType = number | string | bigint
1164
+ [extra: string | symbol]: any;
1165
+ }
1166
+ ```
1167
+
1168
+ Key properties accessed by the authorization module via the index signature:
1169
+ - `user.roles` -- used by `extractUserRoles()` for role-based shortcuts
1170
+ - `user.principalType` -- required by `buildRules()` for enforcer-based evaluation
1171
+
1172
+ ## IJWTTokenPayload Interface
1173
+
1174
+ Full JWT token payload shape (extends `IAuthUser`):
1175
+
1176
+ ```typescript
1177
+ interface IJWTTokenPayload extends JWTPayload, IAuthUser {
1178
+ userId: IdType;
1179
+ roles: { id: IdType; identifier: string; priority: number }[];
1180
+ clientId?: string;
1181
+ provider?: string;
1182
+ email?: string;
1183
+ name?: string;
1184
+ [extra: string | symbol]: any;
1185
+ }
1186
+ ```
1187
+
1188
+ ## Hono Context Variables (ContextVariableMap)
1189
+
1190
+ The auth module augments Hono's `ContextVariableMap` in `auth/context-variables.ts`:
1191
+
1192
+ ```typescript
1193
+ declare module 'hono' {
1194
+ interface ContextVariableMap {
1195
+ // Authentication
1196
+ [Authentication.CURRENT_USER]: IAuthUser; // 'authentication.currentUser'
1197
+ [Authentication.AUDIT_USER_ID]: IdType; // 'authentication.auditUserId'
1198
+ [Authentication.SKIP_AUTHENTICATION]: boolean; // 'authentication.skip'
1199
+
1200
+ // Authorization
1201
+ [Authorization.RULES]: unknown; // 'authorization.rules'
1202
+ [Authorization.SKIP_AUTHORIZATION]: boolean; // 'authorization.skip'
1203
+ }
1204
+ }
1205
+ ```
1206
+
1207
+ This enables type-safe `context.get()` and `context.set()` across all auth middleware.
1208
+
1209
+ ## See Also
1210
+
1211
+ - [Setup & Configuration](./) -- Binding keys, options interfaces, and initial setup
1212
+ - [Usage & Examples](./usage) -- Securing routes, voters, patterns, and CRUD integration
1213
+ - [Error Reference](./errors) -- Error messages and troubleshooting