@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,758 @@
1
+ # Authorization -- Usage & Examples
2
+
3
+ > Securing routes, voters, CRUD factory integration, custom enforcers, and comparable actions/resources. See [Setup & Configuration](./) for initial setup.
4
+
5
+ ## Securing Routes
6
+
7
+ ### Imperative Route (defineRoute)
8
+
9
+ Use the `authorize` field in route configs to declare authorization requirements:
10
+
11
+ ```typescript
12
+ import {
13
+ BaseController,
14
+ Authentication,
15
+ AuthorizationActions,
16
+ } from '@venizia/ignis';
17
+
18
+ class ArticleController extends BaseController {
19
+ binding() {
20
+ // Read requires 'read' action on 'Article' resource
21
+ this.defineRoute({
22
+ configs: {
23
+ path: '/',
24
+ method: 'get',
25
+ authenticate: { strategies: [Authentication.STRATEGY_JWT] },
26
+ authorize: {
27
+ action: AuthorizationActions.READ,
28
+ resource: 'Article',
29
+ },
30
+ responses: jsonResponse({
31
+ description: 'List of articles',
32
+ schema: z.array(ArticleSchema),
33
+ }),
34
+ },
35
+ handler: async (context) => {
36
+ const articles = await this.articleService.findAll();
37
+ return context.json(articles);
38
+ },
39
+ });
40
+
41
+ // Delete requires 'delete' action with conditions
42
+ this.defineRoute({
43
+ configs: {
44
+ path: '/{id}',
45
+ method: 'delete',
46
+ authenticate: { strategies: [Authentication.STRATEGY_JWT] },
47
+ authorize: {
48
+ action: AuthorizationActions.DELETE,
49
+ resource: 'Article',
50
+ conditions: { ownerId: 'currentUser' },
51
+ },
52
+ responses: jsonResponse({
53
+ description: 'Deleted article',
54
+ schema: ArticleSchema,
55
+ }),
56
+ },
57
+ handler: async (context) => {
58
+ const { id } = context.req.valid('param');
59
+ const result = await this.articleService.deleteById({ id });
60
+ return context.json(result);
61
+ },
62
+ });
63
+ }
64
+ }
65
+ ```
66
+
67
+ ### Multiple Authorization Specs
68
+
69
+ Pass an array of `IAuthorizationSpec` to require **all** specs to pass. Each spec creates a separate middleware -- all must succeed for the handler to execute:
70
+
71
+ ```typescript
72
+ import { Authentication, AuthorizationActions } from '@venizia/ignis';
73
+
74
+ this.defineRoute({
75
+ configs: {
76
+ path: '/admin/users/{id}',
77
+ method: 'patch',
78
+ authenticate: { strategies: [Authentication.STRATEGY_JWT] },
79
+ authorize: [
80
+ { action: AuthorizationActions.UPDATE, resource: 'User' },
81
+ { action: AuthorizationActions.UPDATE, resource: 'Admin' },
82
+ ],
83
+ responses: jsonResponse({
84
+ description: 'Updated user',
85
+ schema: UserSchema,
86
+ }),
87
+ },
88
+ handler: async (context) => {
89
+ // Both 'update:User' AND 'update:Admin' must pass
90
+ },
91
+ });
92
+ ```
93
+
94
+ > [!NOTE]
95
+ > When multiple specs are evaluated on the same route, rules are built once and cached on the context (`Authorization.RULES`). The second spec reuses the cached rules without rebuilding.
96
+
97
+ ### Decorator-Based Route
98
+
99
+ Use the `authorize` field alongside `authenticate` in decorator configs:
100
+
101
+ ```typescript
102
+ import { controller, get, post, AuthorizationActions, AuthorizationRoles } from '@venizia/ignis';
103
+
104
+ @controller({ path: '/articles' })
105
+ class ArticleController extends BaseController {
106
+ @get({
107
+ configs: {
108
+ path: '/',
109
+ authenticate: { strategies: [Authentication.STRATEGY_JWT] },
110
+ authorize: { action: AuthorizationActions.READ, resource: 'Article' },
111
+ responses: jsonResponse({ description: 'Articles', schema: z.array(ArticleSchema) }),
112
+ },
113
+ })
114
+ async findAll(opts: { context: TRouteContext }) {
115
+ // Handler runs only if authorized
116
+ }
117
+
118
+ @post({
119
+ configs: {
120
+ path: '/',
121
+ authenticate: { strategies: [Authentication.STRATEGY_JWT] },
122
+ authorize: {
123
+ action: AuthorizationActions.CREATE,
124
+ resource: 'Article',
125
+ allowedRoles: ['editor', AuthorizationRoles.ADMIN.identifier],
126
+ },
127
+ request: { body: jsonContent({ schema: CreateArticleSchema }) },
128
+ responses: jsonResponse({ description: 'Created article', schema: ArticleSchema }),
129
+ },
130
+ })
131
+ async create(opts: { context: TRouteContext }) {
132
+ // Handler runs if user has 'create:Article' permission OR 'editor'/'900_admin' role
133
+ }
134
+ }
135
+ ```
136
+
137
+ ## Using the `authorize()` Standalone Function
138
+
139
+ The `authorize()` function is a convenience wrapper around `AuthorizationProvider`. It returns a Hono `MiddlewareHandler`:
140
+
141
+ ```typescript
142
+ import { authorize, authenticate, Authentication, AuthorizationActions } from '@venizia/ignis';
143
+
144
+ // Use as Hono middleware directly
145
+ app.delete(
146
+ '/articles/:id',
147
+ authenticate({ strategies: [Authentication.STRATEGY_JWT] }),
148
+ authorize({ spec: { action: AuthorizationActions.DELETE, resource: 'Article' } }),
149
+ (c) => {
150
+ const user = c.get(Authentication.CURRENT_USER);
151
+ return c.json({ deleted: true });
152
+ },
153
+ );
154
+ ```
155
+
156
+ ### With Specific Enforcer
157
+
158
+ If multiple enforcers are registered, specify which one to use:
159
+
160
+ ```typescript
161
+ authorize({
162
+ spec: { action: AuthorizationActions.READ, resource: 'Report' },
163
+ enforcerName: 'my-custom', // defaults to first registered if omitted
164
+ });
165
+ ```
166
+
167
+ ## Voters
168
+
169
+ Voters provide custom authorization logic that runs **before** the enforcer (step 4 in the pipeline).
170
+
171
+ ```mermaid
172
+ flowchart TD
173
+ Start([Step 4: Voters]) --> HasVoters{Has voters?}
174
+ HasVoters -->|No| Enforcer([Continue to enforcer])
175
+ HasVoters -->|Yes| V1["Call voter 1"]
176
+ V1 --> D1{Decision?}
177
+ D1 -->|DENY| E403[/403 Forbidden/]
178
+ D1 -->|ALLOW| Next([next - authorized])
179
+ D1 -->|ABSTAIN| V2["Call voter 2"]
180
+ V2 --> D2{Decision?}
181
+ D2 -->|DENY| E403
182
+ D2 -->|ALLOW| Next
183
+ D2 -->|ABSTAIN| VN["... voter N"]
184
+ VN -->|All ABSTAIN| Enforcer
185
+ ```
186
+
187
+ Each voter returns one of three decisions:
188
+
189
+ | Decision | Effect |
190
+ |----------|--------|
191
+ | `AuthorizationDecisions.ALLOW` | Immediately grants access (skips remaining voters and enforcer) |
192
+ | `AuthorizationDecisions.DENY` | Immediately denies access (throws 403) |
193
+ | `AuthorizationDecisions.ABSTAIN` | No opinion -- continues to next voter or enforcer |
194
+
195
+ ### Basic Voter Example
196
+
197
+ ```typescript
198
+ import {
199
+ AuthorizationActions,
200
+ AuthorizationDecisions,
201
+ TAuthorizationVoter,
202
+ } from '@venizia/ignis';
203
+
204
+ const ownerVoter: TAuthorizationVoter = async ({ user, action, resource, context }) => {
205
+ if (action !== AuthorizationActions.UPDATE && action !== AuthorizationActions.DELETE) {
206
+ return AuthorizationDecisions.ABSTAIN;
207
+ }
208
+
209
+ const articleId = context.req.param('id');
210
+ const article = await articleService.findById({ id: articleId });
211
+
212
+ if (!article) {
213
+ return AuthorizationDecisions.ABSTAIN;
214
+ }
215
+
216
+ if (article.authorId === user.userId) {
217
+ return AuthorizationDecisions.ALLOW;
218
+ }
219
+
220
+ return AuthorizationDecisions.ABSTAIN; // Let enforcer decide
221
+ };
222
+ ```
223
+
224
+ ### Using Voters in Routes
225
+
226
+ ```typescript
227
+ this.defineRoute({
228
+ configs: {
229
+ path: '/{id}',
230
+ method: 'patch',
231
+ authenticate: { strategies: [Authentication.STRATEGY_JWT] },
232
+ authorize: {
233
+ action: AuthorizationActions.UPDATE,
234
+ resource: 'Article',
235
+ voters: [ownerVoter],
236
+ },
237
+ // ...
238
+ },
239
+ handler: async (context) => {
240
+ // Runs if: owner (voter ALLOW) OR enforcer permits
241
+ },
242
+ });
243
+ ```
244
+
245
+ ### Multiple Voters
246
+
247
+ Voters are evaluated sequentially. The first non-ABSTAIN decision wins:
248
+
249
+ ```typescript
250
+ authorize: {
251
+ action: AuthorizationActions.UPDATE,
252
+ resource: 'Article',
253
+ voters: [ownerVoter, adminOverrideVoter, timeWindowVoter],
254
+ }
255
+ ```
256
+
257
+ **Evaluation flow:**
258
+ 1. `ownerVoter` returns `ABSTAIN` -- continue
259
+ 2. `adminOverrideVoter` returns `ALLOW` -- **access granted** (skips remaining voters and enforcer)
260
+
261
+ > [!TIP]
262
+ > Use `ABSTAIN` as the default return when a voter doesn't have a strong opinion. Only return `DENY` when you're certain the request should be blocked regardless of other checks.
263
+
264
+ ## Role-Based Shortcuts
265
+
266
+ ### Global `alwaysAllowRoles`
267
+
268
+ Roles listed in `alwaysAllowRoles` bypass **all** authorization checks globally (step 3 in the pipeline):
269
+
270
+ ```typescript
271
+ import { AuthorizationRoles } from '@venizia/ignis';
272
+
273
+ this.bind<IAuthorizeOptions>({ key: AuthorizeBindingKeys.OPTIONS }).toValue({
274
+ defaultDecision: 'deny',
275
+ alwaysAllowRoles: [AuthorizationRoles.SUPER_ADMIN.identifier, 'system'],
276
+ });
277
+ ```
278
+
279
+ ### Per-Route `allowedRoles`
280
+
281
+ Roles listed in `allowedRoles` on a specific `IAuthorizationSpec` bypass the enforcer for that route only (still evaluated at step 3):
282
+
283
+ ```typescript
284
+ authorize: {
285
+ action: AuthorizationActions.DELETE,
286
+ resource: 'Article',
287
+ allowedRoles: [AuthorizationRoles.ADMIN.identifier, 'moderator'],
288
+ }
289
+ ```
290
+
291
+ ### Role Extraction
292
+
293
+ The authorization middleware extracts roles from the authenticated user's `roles` field via the `extractUserRoles()` method:
294
+
295
+ ```mermaid
296
+ flowchart TD
297
+ Input["user.roles"] --> IsArray{Array?}
298
+ IsArray -->|No| Empty(["return []"])
299
+ IsArray -->|Yes| Map["Map each role"]
300
+ Map --> Type{Type?}
301
+ Type -->|string| AsIs["Use as-is"]
302
+ Type -->|object| Prio["r.identifier"]
303
+ Prio -->|undefined| Name["r.name"]
304
+ Name -->|undefined| Id["String(r.id)"]
305
+ ```
306
+
307
+ It supports multiple formats:
308
+
309
+ ```typescript
310
+ // String array
311
+ roles: ['admin', 'user']
312
+
313
+ // Object array with identifier (preferred — matches AuthorizationRole.identifier)
314
+ roles: [{ id: 1, identifier: '900_admin', priority: 900 }]
315
+
316
+ // Object array with name fallback
317
+ roles: [{ id: 1, name: 'admin' }]
318
+
319
+ // Object array with id-only fallback
320
+ roles: [{ id: 1 }]
321
+ ```
322
+
323
+ Extraction priority: `identifier` > `name` > `String(id)`
324
+
325
+ ## CRUD Factory Integration
326
+
327
+ ### Controller-Level Authorization
328
+
329
+ Apply authorization to all CRUD routes:
330
+
331
+ ```typescript
332
+ import { AuthorizationActions } from '@venizia/ignis';
333
+
334
+ ControllerFactory.defineCrudController({
335
+ entity: Article,
336
+ repository: { name: 'ArticleRepository' },
337
+ controller: { name: 'ArticleController', basePath: '/articles' },
338
+ authenticate: { strategies: [Authentication.STRATEGY_JWT] },
339
+ authorize: { action: AuthorizationActions.READ, resource: 'Article' },
340
+ });
341
+ ```
342
+
343
+ ### Per-Route Overrides
344
+
345
+ Override authorization per CRUD endpoint:
346
+
347
+ ```typescript
348
+ import { AuthorizationActions, AuthorizationRoles } from '@venizia/ignis';
349
+
350
+ ControllerFactory.defineCrudController({
351
+ entity: Article,
352
+ repository: { name: 'ArticleRepository' },
353
+ controller: { name: 'ArticleController', basePath: '/articles' },
354
+ authenticate: { strategies: [Authentication.STRATEGY_JWT] },
355
+ authorize: { action: AuthorizationActions.READ, resource: 'Article' },
356
+ routes: {
357
+ // Public read -- skip both auth
358
+ find: { authenticate: { skip: true } },
359
+ count: { authenticate: { skip: true } },
360
+
361
+ // Custom authorization for write operations
362
+ create: {
363
+ authorize: { action: AuthorizationActions.CREATE, resource: 'Article' },
364
+ },
365
+ updateById: {
366
+ authorize: { action: AuthorizationActions.UPDATE, resource: 'Article' },
367
+ },
368
+
369
+ // Skip only authorization (still requires auth)
370
+ findOne: { authorize: { skip: true } },
371
+
372
+ // Strict delete with custom roles
373
+ deleteById: {
374
+ authorize: {
375
+ action: AuthorizationActions.DELETE,
376
+ resource: 'Article',
377
+ allowedRoles: [AuthorizationRoles.ADMIN.identifier],
378
+ },
379
+ },
380
+ },
381
+ });
382
+ ```
383
+
384
+ ### Priority Resolution (Factory Routes)
385
+
386
+ The `defineControllerRouteConfigs` function resolves authorization with this priority:
387
+
388
+ ```mermaid
389
+ flowchart TD
390
+ Route([Route config]) --> AuthSkip{"authenticate:<br/>{ skip: true }?"}
391
+ AuthSkip -->|Yes| NoAuth([Skip BOTH<br/>auth + authz])
392
+ AuthSkip -->|No| AuthzSkip{"authorize:<br/>{ skip: true }?"}
393
+ AuthzSkip -->|Yes| NoAuthz([Skip authz only])
394
+ AuthzSkip -->|No| PerRoute{"Per-route<br/>authorize spec?"}
395
+ PerRoute -->|Yes| UseRoute([Use per-route spec])
396
+ PerRoute -->|No| Controller{"Controller-level<br/>authorize?"}
397
+ Controller -->|Yes| UseCtrl([Use controller spec])
398
+ Controller -->|No| NoAuthz2([No authorization])
399
+ ```
400
+
401
+ 1. **`authenticate: { skip: true }`** -- skips both authentication and authorization
402
+ 2. **`authorize: { skip: true }`** -- skips authorization only
403
+ 3. **Per-route `authorize` spec** -- overrides controller-level
404
+ 4. **Controller-level `authorize`** -- default for all routes
405
+
406
+ ## Dynamic Skip Authorization
407
+
408
+ Use `Authorization.SKIP_AUTHORIZATION` to dynamically bypass authorization in middleware (step 1 in the pipeline):
409
+
410
+ ```typescript
411
+ import { Authorization } from '@venizia/ignis';
412
+ import { createMiddleware } from 'hono/factory';
413
+
414
+ const conditionalAuthzMiddleware = createMiddleware(async (c, next) => {
415
+ // Skip authorization for internal service-to-service calls
416
+ if (c.req.header('X-Internal-Service') === 'trusted-key') {
417
+ c.set(Authorization.SKIP_AUTHORIZATION, true);
418
+ }
419
+ return next();
420
+ });
421
+ ```
422
+
423
+ ## Rules Caching
424
+
425
+ The authorization middleware caches rules on the Hono context to avoid rebuilding them on every authorization spec evaluation. This is especially useful when multiple authorization specs are applied to the same route:
426
+
427
+ ```typescript
428
+ // First spec triggers buildRules() → result cached on context
429
+ authorize: [
430
+ { action: AuthorizationActions.READ, resource: 'Article' },
431
+ { action: AuthorizationActions.READ, resource: 'Comment' },
432
+ ]
433
+ // Second spec reuses cached rules → no rebuild
434
+ ```
435
+
436
+ > [!TIP]
437
+ > Rules caching happens per-request. Each new HTTP request starts with an empty cache. If you need to invalidate cached rules mid-request (e.g., after role change), set `context.set(Authorization.RULES, null)`.
438
+
439
+ ## Accessing Context Variables
440
+
441
+ The authorization module provides type-safe access to auth data on the Hono context:
442
+
443
+ ```typescript
444
+ import { Authorization, Authentication } from '@venizia/ignis';
445
+
446
+ // In a route handler or middleware
447
+ const user = c.get(Authentication.CURRENT_USER); // IAuthUser
448
+ const rules = c.get(Authorization.RULES); // unknown (type depends on enforcer)
449
+ const isSkipped = c.get(Authorization.SKIP_AUTHORIZATION); // boolean
450
+
451
+ // Set skip dynamically
452
+ c.set(Authorization.SKIP_AUTHORIZATION, true);
453
+
454
+ // Invalidate cached rules
455
+ c.set(Authorization.RULES, null);
456
+ ```
457
+
458
+ ## Using IAuthorizationComparable
459
+
460
+ For custom action/resource comparison logic beyond plain string equality, implement `IAuthorizationComparable`.
461
+
462
+ ### StringAuthorizationAction with Wildcard
463
+
464
+ The built-in `StringAuthorizationAction` supports a wildcard (`*`) that matches any action:
465
+
466
+ ```typescript
467
+ import { StringAuthorizationAction } from '@venizia/ignis';
468
+
469
+ const wildcard = StringAuthorizationAction.build({ value: '*' });
470
+ wildcard.isEqual('read'); // true — wildcard matches all
471
+ wildcard.isEqual('delete'); // true — wildcard matches all
472
+ wildcard.isEqual('create'); // true — wildcard matches all
473
+
474
+ const readOnly = StringAuthorizationAction.build({ value: 'read' });
475
+ readOnly.isEqual('read'); // true
476
+ readOnly.isEqual('update'); // false
477
+ ```
478
+
479
+ ### StringAuthorizationResource
480
+
481
+ Standard string comparison for resources (no wildcard):
482
+
483
+ ```typescript
484
+ import { StringAuthorizationResource } from '@venizia/ignis';
485
+
486
+ const article = StringAuthorizationResource.build({ value: 'Article' });
487
+ article.isEqual('Article'); // true
488
+ article.isEqual('User'); // false
489
+ ```
490
+
491
+ ### Custom Comparable Implementation
492
+
493
+ Create your own comparable type for advanced matching:
494
+
495
+ ```typescript
496
+ import type { IAuthorizationComparable } from '@venizia/ignis';
497
+
498
+ class HierarchicalResource implements IAuthorizationComparable<string> {
499
+ readonly value: string;
500
+
501
+ constructor(opts: { value: string }) {
502
+ this.value = opts.value;
503
+ }
504
+
505
+ compare(other: string): number {
506
+ // Match if the other resource starts with this resource's value
507
+ // e.g., 'articles' matches 'articles.comments'
508
+ if (other.startsWith(this.value)) return 0;
509
+ return this.value.localeCompare(other);
510
+ }
511
+
512
+ isEqual(other: string): boolean {
513
+ return this.compare(other) === 0;
514
+ }
515
+ }
516
+ ```
517
+
518
+ ## Custom Enforcer
519
+
520
+ Create a custom enforcer by implementing `IAuthorizationEnforcer`:
521
+
522
+ ```typescript
523
+ import {
524
+ IAuthorizationEnforcer,
525
+ IAuthorizationRequest,
526
+ IAuthUser,
527
+ TAuthorizationDecision,
528
+ AuthorizationDecisions,
529
+ TContext,
530
+ } from '@venizia/ignis';
531
+ import { BaseHelper, ValueOrPromise } from '@venizia/ignis-helpers';
532
+ import { Env } from 'hono';
533
+
534
+ type MyRules = Map<string, Set<string>>;
535
+
536
+ class MyCustomEnforcer
537
+ extends BaseHelper
538
+ implements IAuthorizationEnforcer<Env, string, string, MyRules>
539
+ {
540
+ name = 'my-custom';
541
+
542
+ constructor() {
543
+ super({ scope: MyCustomEnforcer.name });
544
+ }
545
+
546
+ async configure(): Promise<void> {
547
+ // One-time initialization (called by registry on first use)
548
+ }
549
+
550
+ async buildRules(opts: {
551
+ user: { principalType: string } & IAuthUser;
552
+ context: TContext;
553
+ }): Promise<MyRules> {
554
+ const rules = new Map<string, Set<string>>();
555
+ // Build your rules map from DB, config, etc.
556
+ return rules;
557
+ }
558
+
559
+ async evaluate(opts: {
560
+ rules: MyRules;
561
+ request: IAuthorizationRequest;
562
+ context: TContext;
563
+ }): Promise<TAuthorizationDecision> {
564
+ const { rules, request } = opts;
565
+ const resourceActions = rules.get(request.resource);
566
+ if (resourceActions?.has(request.action)) {
567
+ return AuthorizationDecisions.ALLOW;
568
+ }
569
+ return AuthorizationDecisions.DENY;
570
+ }
571
+ }
572
+ ```
573
+
574
+ Then register it via the registry:
575
+
576
+ ```typescript
577
+ import {
578
+ AuthorizationEnforcerRegistry,
579
+ AuthorizationEnforcerTypes,
580
+ AuthorizeBindingKeys,
581
+ AuthorizeComponent,
582
+ IAuthorizeOptions,
583
+ } from '@venizia/ignis';
584
+
585
+ // Step 1: Global options
586
+ this.bind<IAuthorizeOptions>({ key: AuthorizeBindingKeys.OPTIONS }).toValue({
587
+ defaultDecision: 'deny',
588
+ });
589
+
590
+ // Step 2: Component
591
+ this.component(AuthorizeComponent);
592
+
593
+ // Step 3: Register custom enforcer
594
+ AuthorizationEnforcerRegistry.getInstance().register({
595
+ container: this,
596
+ enforcers: [{
597
+ enforcer: MyCustomEnforcer,
598
+ name: 'my-custom',
599
+ type: AuthorizationEnforcerTypes.CUSTOM,
600
+ options: { /* your enforcer-specific options if needed */ },
601
+ }],
602
+ });
603
+ ```
604
+
605
+ > [!NOTE]
606
+ > Custom enforcers can inject their options via `@inject({ key: AuthorizeBindingKeys.enforcerOptions('my-custom') })` in the constructor, just like `CasbinAuthorizationEnforcer` does.
607
+
608
+ ## Custom Filtered Adapter
609
+
610
+ Create a custom adapter by extending `BaseFilteredAdapter`:
611
+
612
+ ```typescript
613
+ import {
614
+ BaseFilteredAdapter,
615
+ IBaseFilteredAdapterEntities,
616
+ ICasbinPolicyFilter,
617
+ TBasePolicyRow,
618
+ } from '@venizia/ignis';
619
+
620
+ interface MyEntities extends IBaseFilteredAdapterEntities {
621
+ permission: { tableName: string; principalType: string };
622
+ role: { tableName: string; principalType: string };
623
+ policyDefinition: { tableName: string; principalType: string };
624
+ }
625
+
626
+ class MyCustomAdapter extends BaseFilteredAdapter<MyEntities> {
627
+ constructor(opts: { entities: MyEntities; /* your dependencies */ }) {
628
+ super({ scope: MyCustomAdapter.name, entities: opts.entities });
629
+ }
630
+
631
+ protected async buildDirectPolicies(opts: {
632
+ filter: ICasbinPolicyFilter;
633
+ rolePrincipal: string;
634
+ }): Promise<string[]> {
635
+ // Query direct permission policies for the user
636
+ // Return casbin `p` lines using this.toPolicyLine()
637
+ const rows = await this.queryDirectPolicies(opts.filter);
638
+ return rows.map(row => this.toPolicyLine({ row })).filter(Boolean) as string[];
639
+ }
640
+
641
+ protected async buildGroupPolicies(opts: {
642
+ filter: ICasbinPolicyFilter;
643
+ }): Promise<{ lines: string[]; roleIds: (string | number)[] }> {
644
+ // Query role assignments for the user
645
+ // Return casbin `g` lines using this.toGroupLine() + role IDs
646
+ return { lines: [...], roleIds: [...] };
647
+ }
648
+
649
+ protected async buildRolePolicies(opts: {
650
+ roleIds: (string | number)[];
651
+ rolePrincipal: string;
652
+ }): Promise<string[]> {
653
+ // Query permission policies inherited through roles
654
+ // Return casbin `p` lines using this.toPolicyLine()
655
+ return [...];
656
+ }
657
+ }
658
+ ```
659
+
660
+ The base class provides shared formatters:
661
+ - `this.formatDomain(domain)` -- adds entity prefix to domain values
662
+ - `this.toGroupLine({ subject, role, domain })` -- formats `g` lines
663
+ - `this.toPolicyLine({ row })` -- formats `p` lines
664
+
665
+ ## AuthorizationRole Comparison
666
+
667
+ Use `AuthorizationRole` for priority-based role comparison:
668
+
669
+ ```typescript
670
+ import { AuthorizationRole, AuthorizationRoles } from '@venizia/ignis';
671
+
672
+ // Built-in roles
673
+ AuthorizationRoles.SUPER_ADMIN.identifier; // '999_super-admin'
674
+ AuthorizationRoles.ADMIN.identifier; // '900_admin'
675
+ AuthorizationRoles.USER.identifier; // '010_user'
676
+
677
+ // Comparison
678
+ AuthorizationRoles.SUPER_ADMIN.isHigherThan({ target: AuthorizationRoles.ADMIN }); // true
679
+ AuthorizationRoles.GUEST.isLowerThan({ target: AuthorizationRoles.USER }); // true
680
+
681
+ // Custom roles
682
+ const moderator = AuthorizationRole.build({ name: 'moderator', priority: 500 });
683
+ moderator.identifier; // '500_moderator'
684
+ moderator.isHigherThan({ target: AuthorizationRoles.USER }); // true (500 > 10)
685
+ moderator.isLowerThan({ target: AuthorizationRoles.ADMIN }); // true (500 < 900)
686
+
687
+ // Custom delimiter
688
+ const customRole = AuthorizationRole.build({ name: 'editor', priority: 100, delimiter: '-' });
689
+ customRole.identifier; // '100-editor'
690
+ ```
691
+
692
+ ## Model-Based Resource References
693
+
694
+ Instead of hardcoding resource strings, use `AUTHORIZATION_SUBJECT` from your model classes. When a model declares `authorize.principal` in `@model` settings, the decorator auto-populates `AUTHORIZATION_SUBJECT`:
695
+
696
+ ```typescript
697
+ import { BaseEntity, model, generateIdColumnDefs } from '@venizia/ignis';
698
+ import { pgTable, text } from 'drizzle-orm/pg-core';
699
+
700
+ @model({
701
+ type: 'entity',
702
+ settings: {
703
+ authorize: { principal: 'article' },
704
+ },
705
+ })
706
+ export class Article extends BaseEntity<typeof Article.schema> {
707
+ static override schema = pgTable('Article', {
708
+ ...generateIdColumnDefs({ id: { dataType: 'string' } }),
709
+ title: text('title').notNull(),
710
+ });
711
+ }
712
+
713
+ // Article.AUTHORIZATION_SUBJECT === 'article'
714
+ ```
715
+
716
+ Use it in route configs for type-safe, refactor-friendly resource references:
717
+
718
+ ```typescript
719
+ import { AuthorizationActions } from '@venizia/ignis';
720
+ import { Article } from '../models/entities/article.model';
721
+
722
+ // Instead of: resource: 'article'
723
+ authorize: {
724
+ action: AuthorizationActions.READ,
725
+ resource: Article.AUTHORIZATION_SUBJECT,
726
+ }
727
+ ```
728
+
729
+ ### Querying All Principals
730
+
731
+ Use `MetadataRegistry` to retrieve all registered authorization principals at runtime:
732
+
733
+ ```typescript
734
+ import { MetadataRegistry } from '@venizia/ignis';
735
+
736
+ const registry = MetadataRegistry.getInstance();
737
+
738
+ // Flat array of principal names — ideal for Casbin policy setup
739
+ const principals = registry.getAuthorizeModelPrincipals({ format: 'array' });
740
+ // ['article', 'user', 'configuration']
741
+
742
+ // Record of model name → principal
743
+ const principalMap = registry.getAuthorizeModelPrincipals({ format: 'record' });
744
+ // { Article: 'article', User: 'user', Configuration: 'configuration' }
745
+
746
+ // Full settings with model registry entries (framework-level)
747
+ const settings = registry.getAuthorizeModelSettings({ format: 'array' });
748
+ // [{ name: 'Article', authorize: { principal: 'article' }, entry: IModelRegistryEntry }]
749
+ ```
750
+
751
+ > [!TIP]
752
+ > Defining `authorize.principal` on the model makes the model the single source of truth for its authorization subject. This eliminates string duplication across route configs and policy setup.
753
+
754
+ ## See Also
755
+
756
+ - [Setup & Configuration](./) -- Binding keys, options interfaces, and initial setup
757
+ - [API Reference](./api) -- Architecture, enforcer internals, provider, registry, and adapters
758
+ - [Error Reference](./errors) -- Error messages and troubleshooting