@venizia/ignis-docs 0.0.7 → 0.0.8-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 (158) hide show
  1. package/dist/mcp-server/common/paths.d.ts +4 -2
  2. package/dist/mcp-server/common/paths.d.ts.map +1 -1
  3. package/dist/mcp-server/common/paths.js +8 -6
  4. package/dist/mcp-server/common/paths.js.map +1 -1
  5. package/dist/mcp-server/tools/docs/get-document-content.tool.d.ts +1 -1
  6. package/dist/mcp-server/tools/docs/get-document-content.tool.d.ts.map +1 -1
  7. package/dist/mcp-server/tools/docs/get-document-content.tool.js +7 -7
  8. package/dist/mcp-server/tools/docs/get-document-metadata.tool.js +3 -3
  9. package/dist/mcp-server/tools/docs/get-package-overview.tool.d.ts +1 -1
  10. package/dist/mcp-server/tools/docs/get-package-overview.tool.js +1 -1
  11. package/package.json +1 -1
  12. package/wiki/best-practices/api-usage-examples.md +9 -9
  13. package/wiki/best-practices/architectural-patterns.md +19 -3
  14. package/wiki/best-practices/architecture-decisions.md +6 -6
  15. package/wiki/best-practices/code-style-standards/advanced-patterns.md +1 -1
  16. package/wiki/best-practices/code-style-standards/control-flow.md +1 -1
  17. package/wiki/best-practices/code-style-standards/function-patterns.md +2 -2
  18. package/wiki/best-practices/code-style-standards/index.md +2 -2
  19. package/wiki/best-practices/code-style-standards/naming-conventions.md +1 -1
  20. package/wiki/best-practices/code-style-standards/route-definitions.md +4 -4
  21. package/wiki/best-practices/data-modeling.md +1 -1
  22. package/wiki/best-practices/deployment-strategies.md +1 -1
  23. package/wiki/best-practices/error-handling.md +2 -2
  24. package/wiki/best-practices/performance-optimization.md +3 -3
  25. package/wiki/best-practices/security-guidelines.md +2 -2
  26. package/wiki/best-practices/troubleshooting-tips.md +1 -1
  27. package/wiki/{references → extensions}/components/authentication/api.md +12 -20
  28. package/wiki/{references → extensions}/components/authentication/errors.md +1 -1
  29. package/wiki/{references → extensions}/components/authentication/index.md +5 -8
  30. package/wiki/{references → extensions}/components/authentication/usage.md +20 -36
  31. package/wiki/{references → extensions}/components/authorization/api.md +62 -13
  32. package/wiki/{references → extensions}/components/authorization/errors.md +12 -7
  33. package/wiki/{references → extensions}/components/authorization/index.md +93 -6
  34. package/wiki/{references → extensions}/components/authorization/usage.md +42 -4
  35. package/wiki/{references → extensions}/components/health-check.md +5 -4
  36. package/wiki/{references → extensions}/components/index.md +2 -0
  37. package/wiki/{references → extensions}/components/mail/index.md +1 -1
  38. package/wiki/{references → extensions}/components/request-tracker.md +1 -1
  39. package/wiki/{references → extensions}/components/socket-io/api.md +2 -2
  40. package/wiki/{references → extensions}/components/socket-io/errors.md +2 -0
  41. package/wiki/{references → extensions}/components/socket-io/index.md +24 -20
  42. package/wiki/{references → extensions}/components/socket-io/usage.md +2 -2
  43. package/wiki/{references → extensions}/components/static-asset/api.md +14 -15
  44. package/wiki/{references → extensions}/components/static-asset/errors.md +3 -1
  45. package/wiki/{references → extensions}/components/static-asset/index.md +158 -89
  46. package/wiki/{references → extensions}/components/static-asset/usage.md +8 -5
  47. package/wiki/{references → extensions}/components/swagger.md +3 -3
  48. package/wiki/{references → extensions}/components/template/index.md +4 -4
  49. package/wiki/{references → extensions}/components/template/setup-page.md +1 -1
  50. package/wiki/{references → extensions}/components/template/single-page.md +1 -1
  51. package/wiki/{references → extensions}/components/websocket/api.md +7 -6
  52. package/wiki/{references → extensions}/components/websocket/errors.md +17 -3
  53. package/wiki/{references → extensions}/components/websocket/index.md +17 -11
  54. package/wiki/{references → extensions}/components/websocket/usage.md +2 -2
  55. package/wiki/{references → extensions}/helpers/crypto/index.md +1 -1
  56. package/wiki/{references → extensions}/helpers/env/index.md +9 -5
  57. package/wiki/{references → extensions}/helpers/error/index.md +2 -7
  58. package/wiki/{references → extensions}/helpers/index.md +18 -6
  59. package/wiki/{references → extensions}/helpers/kafka/admin.md +13 -1
  60. package/wiki/{references → extensions}/helpers/kafka/consumer.md +28 -28
  61. package/wiki/{references → extensions}/helpers/kafka/examples.md +19 -19
  62. package/wiki/{references → extensions}/helpers/kafka/index.md +51 -48
  63. package/wiki/{references → extensions}/helpers/kafka/producer.md +18 -18
  64. package/wiki/{references → extensions}/helpers/kafka/schema-registry.md +25 -25
  65. package/wiki/{references → extensions}/helpers/logger/index.md +2 -2
  66. package/wiki/{references → extensions}/helpers/queue/index.md +400 -4
  67. package/wiki/{references → extensions}/helpers/storage/api.md +170 -10
  68. package/wiki/{references → extensions}/helpers/storage/index.md +44 -8
  69. package/wiki/{references → extensions}/helpers/template/index.md +1 -1
  70. package/wiki/{references → extensions}/helpers/testing/index.md +4 -4
  71. package/wiki/{references → extensions}/helpers/types/index.md +63 -16
  72. package/wiki/{references → extensions}/helpers/websocket/index.md +1 -1
  73. package/wiki/extensions/index.md +48 -0
  74. package/wiki/guides/core-concepts/application/bootstrapping.md +55 -37
  75. package/wiki/guides/core-concepts/application/index.md +95 -35
  76. package/wiki/guides/core-concepts/components-guide.md +23 -19
  77. package/wiki/guides/core-concepts/components.md +34 -10
  78. package/wiki/guides/core-concepts/dependency-injection.md +99 -34
  79. package/wiki/guides/core-concepts/grpc-controllers.md +295 -0
  80. package/wiki/guides/core-concepts/persistent/datasources.md +27 -8
  81. package/wiki/guides/core-concepts/persistent/models.md +43 -1
  82. package/wiki/guides/core-concepts/persistent/repositories.md +75 -8
  83. package/wiki/guides/core-concepts/persistent/transactions.md +38 -8
  84. package/wiki/guides/core-concepts/{controllers.md → rest-controllers.md} +30 -33
  85. package/wiki/guides/core-concepts/services.md +19 -5
  86. package/wiki/guides/get-started/5-minute-quickstart.md +6 -7
  87. package/wiki/guides/get-started/philosophy.md +1 -1
  88. package/wiki/guides/index.md +2 -2
  89. package/wiki/guides/reference/glossary.md +7 -7
  90. package/wiki/guides/reference/mcp-docs-server.md +1 -1
  91. package/wiki/guides/tutorials/building-a-crud-api.md +2 -2
  92. package/wiki/guides/tutorials/complete-installation.md +17 -14
  93. package/wiki/guides/tutorials/ecommerce-api.md +18 -18
  94. package/wiki/guides/tutorials/realtime-chat.md +8 -8
  95. package/wiki/guides/tutorials/testing.md +2 -2
  96. package/wiki/index.md +4 -3
  97. package/wiki/references/base/application.md +341 -21
  98. package/wiki/references/base/bootstrapping.md +43 -13
  99. package/wiki/references/base/components.md +259 -8
  100. package/wiki/references/base/controllers.md +556 -253
  101. package/wiki/references/base/datasources.md +159 -79
  102. package/wiki/references/base/dependency-injection.md +299 -48
  103. package/wiki/references/base/filter-system/application-usage.md +18 -2
  104. package/wiki/references/base/filter-system/array-operators.md +14 -6
  105. package/wiki/references/base/filter-system/comparison-operators.md +9 -3
  106. package/wiki/references/base/filter-system/default-filter.md +28 -3
  107. package/wiki/references/base/filter-system/fields-order-pagination.md +17 -13
  108. package/wiki/references/base/filter-system/index.md +169 -11
  109. package/wiki/references/base/filter-system/json-filtering.md +51 -18
  110. package/wiki/references/base/filter-system/list-operators.md +4 -3
  111. package/wiki/references/base/filter-system/logical-operators.md +7 -2
  112. package/wiki/references/base/filter-system/null-operators.md +50 -0
  113. package/wiki/references/base/filter-system/quick-reference.md +82 -243
  114. package/wiki/references/base/filter-system/range-operators.md +7 -1
  115. package/wiki/references/base/filter-system/tips.md +34 -7
  116. package/wiki/references/base/filter-system/use-cases.md +6 -5
  117. package/wiki/references/base/grpc-controllers.md +984 -0
  118. package/wiki/references/base/index.md +32 -24
  119. package/wiki/references/base/middleware.md +347 -0
  120. package/wiki/references/base/models.md +390 -46
  121. package/wiki/references/base/providers.md +14 -14
  122. package/wiki/references/base/repositories/advanced.md +84 -69
  123. package/wiki/references/base/repositories/index.md +447 -12
  124. package/wiki/references/base/repositories/mixins.md +103 -98
  125. package/wiki/references/base/repositories/relations.md +129 -45
  126. package/wiki/references/base/repositories/soft-deletable.md +104 -23
  127. package/wiki/references/base/services.md +94 -14
  128. package/wiki/references/index.md +12 -10
  129. package/wiki/references/quick-reference.md +98 -65
  130. package/wiki/references/utilities/crypto.md +21 -4
  131. package/wiki/references/utilities/date.md +25 -7
  132. package/wiki/references/utilities/index.md +26 -24
  133. package/wiki/references/utilities/jsx.md +54 -54
  134. package/wiki/references/utilities/module.md +8 -6
  135. package/wiki/references/utilities/parse.md +16 -9
  136. package/wiki/references/utilities/performance.md +22 -7
  137. package/wiki/references/utilities/promise.md +19 -16
  138. package/wiki/references/utilities/request.md +48 -26
  139. package/wiki/references/utilities/schema.md +69 -6
  140. package/wiki/references/utilities/statuses.md +131 -140
  141. /package/wiki/{references → extensions}/components/mail/api.md +0 -0
  142. /package/wiki/{references → extensions}/components/mail/errors.md +0 -0
  143. /package/wiki/{references → extensions}/components/mail/usage.md +0 -0
  144. /package/wiki/{references → extensions}/components/template/api-page.md +0 -0
  145. /package/wiki/{references → extensions}/components/template/errors-page.md +0 -0
  146. /package/wiki/{references → extensions}/components/template/usage-page.md +0 -0
  147. /package/wiki/{references → extensions}/helpers/cron/index.md +0 -0
  148. /package/wiki/{references → extensions}/helpers/inversion/index.md +0 -0
  149. /package/wiki/{references → extensions}/helpers/network/api.md +0 -0
  150. /package/wiki/{references → extensions}/helpers/network/index.md +0 -0
  151. /package/wiki/{references → extensions}/helpers/redis/index.md +0 -0
  152. /package/wiki/{references → extensions}/helpers/socket-io/api.md +0 -0
  153. /package/wiki/{references → extensions}/helpers/socket-io/index.md +0 -0
  154. /package/wiki/{references → extensions}/helpers/template/single-page.md +0 -0
  155. /package/wiki/{references → extensions}/helpers/uid/index.md +0 -0
  156. /package/wiki/{references → extensions}/helpers/websocket/api.md +0 -0
  157. /package/wiki/{references → extensions}/helpers/worker-thread/index.md +0 -0
  158. /package/wiki/{references → extensions}/src-details/mcp-server.md +0 -0
@@ -350,8 +350,7 @@ import {
350
350
  extraUserColumns,
351
351
  extraRoleColumns,
352
352
  extraPermissionColumns,
353
- extraPermissionMappingColumns,
354
- extraUserRoleColumns,
353
+ extraPolicyDefinitionColumns,
355
354
  } from '@venizia/ignis';
356
355
  import { withSerialId, withTimestamps } from '@venizia/ignis';
357
356
 
@@ -379,16 +378,11 @@ export const permissions = pgTable('permissions', {
379
378
  ...extraPermissionColumns(),
380
379
  });
381
380
 
382
- // Permission mapping (role-to-permission or user-to-permission)
383
- export const permissionMappings = pgTable('permission_mappings', {
384
- ...withSerialId(),
385
- ...extraPermissionMappingColumns(),
386
- });
387
-
388
- // User-role junction table
389
- export const userRoles = pgTable('user_roles', {
381
+ // Policy definition table (Casbin-style policies)
382
+ export const policyDefinitions = pgTable('policy_definitions', {
390
383
  ...withSerialId(),
391
- ...extraUserRoleColumns(),
384
+ ...withTimestamps(),
385
+ ...extraPolicyDefinitionColumns(),
392
386
  });
393
387
  ```
394
388
 
@@ -435,41 +429,31 @@ extraPermissionColumns(opts?: { idType: 'string' | 'number' })
435
429
 
436
430
  | Column | Type | Default | Description |
437
431
  |--------|------|---------|-------------|
438
- | `code` | `text` | -- | Unique permission code |
432
+ | `code` | `text` (unique) | -- | Unique permission code |
439
433
  | `name` | `text` | -- | Permission name |
440
434
  | `subject` | `text` | -- | Permission subject (e.g., `'User'`, `'Order'`) |
441
- | `pType` | `text` | -- | Permission type (maps to DB column `p_type`) |
442
435
  | `action` | `text` | -- | Permitted action (e.g., `'read'`, `'write'`) |
443
436
  | `scope` | `text` | -- | Permission scope |
444
437
  | `parentId` | `text` or `integer` | `null` | Parent permission ID |
445
438
 
446
- ### extraPermissionMappingColumns
447
-
448
- Returns columns for mapping permissions to users or roles.
449
-
450
- ```typescript
451
- extraPermissionMappingColumns(opts?: { idType: 'string' | 'number' })
452
- ```
453
-
454
- | Column | Type | Default | Description |
455
- |--------|------|---------|-------------|
456
- | `effect` | `text` | `null` | Permission effect (e.g., `'allow'`, `'deny'`) |
457
- | `userId` | `text` or `integer` | `null` | Associated user ID |
458
- | `roleId` | `text` or `integer` | `null` | Associated role ID |
459
- | `permissionId` | `text` or `integer` | -- | Associated permission ID (not null) |
460
-
461
- ### extraUserRoleColumns
439
+ ### extraPolicyDefinitionColumns
462
440
 
463
- Returns columns for the user-role junction table. Includes principal columns (polymorphic type/ID fields) via `generatePrincipalColumnDefs` with a default polymorphic value of `'Role'`.
441
+ Returns columns for Casbin-style policy definitions that map subjects (users/roles) to targets (resources/permissions).
464
442
 
465
443
  ```typescript
466
- extraUserRoleColumns(opts?: { idType: 'string' | 'number' })
444
+ extraPolicyDefinitionColumns(opts?: { idType: 'string' | 'number' })
467
445
  ```
468
446
 
469
- | Column | Type | Default | Description |
470
- |--------|------|---------|-------------|
471
- | _(principal columns)_ | _various_ | -- | Polymorphic columns from `generatePrincipalColumnDefs` |
472
- | `userId` | `text` or `integer` | -- | Associated user ID (not null) |
447
+ | Column | DB Column | Type | Nullable | Default | Description |
448
+ |--------|-----------|------|----------|---------|-------------|
449
+ | `variant` | `variant` | `text` | No | -- | Policy variant (e.g., `'p'` for policy, `'g'` for grouping) |
450
+ | `subjectType` | `subject_type` | `text` | No | -- | Type of subject (e.g., `'user'`, `'role'`) |
451
+ | `targetType` | `target_type` | `text` | No | -- | Type of target (e.g., `'permission'`, `'role'`) |
452
+ | `action` | `action` | `text` | Yes | `null` | Policy action |
453
+ | `effect` | `effect` | `text` | Yes | `null` | Policy effect (e.g., `'allow'`, `'deny'`) |
454
+ | `domain` | `domain` | `text` | Yes | `null` | Policy domain for multi-tenancy |
455
+ | `subjectId` | `subject_id` | `text` or `integer` | No | -- | Subject ID (type depends on `idType`) |
456
+ | `targetId` | `target_id` | `text` or `integer` | No | -- | Target ID (type depends on `idType`) |
473
457
 
474
458
  ### ID Type Polymorphism
475
459
 
@@ -701,7 +685,7 @@ flowchart TD
701
685
  **`all` mode:**
702
686
  - Every strategy must pass successfully
703
687
  - If any strategy fails, the request is immediately rejected (exception propagates)
704
- - The last strategy's user payload is used
688
+ - The **first** strategy's user payload is used as the identity source
705
689
  - **Use case:** Multi-factor authentication (both JWT and Basic required)
706
690
 
707
691
  > [!TIP]
@@ -51,7 +51,9 @@ flowchart TD
51
51
  Roles -->|No match| Voters{Has voters?}
52
52
  Voters -->|DENY| E403a[/403 Denied by voter/]
53
53
  Voters -->|ALLOW| Next4([next - voter allow])
54
- Voters -->|ABSTAIN / none| Resolve[Resolve enforcer by name]
54
+ Voters -->|ABSTAIN / none| HasEnforcers{Enforcers registered?}
55
+ HasEnforcers -->|No| Next6([next - skip, no enforcers])
56
+ HasEnforcers -->|Yes| Resolve[Resolve enforcer by name]
55
57
  Resolve --> Cache{Rules cached?}
56
58
  Cache -->|Yes| Evaluate
57
59
  Cache -->|No| PType{principalType?}
@@ -91,6 +93,7 @@ classDiagram
91
93
  -configuredEnforcers: Set
92
94
  +getInstance()$ AuthorizationEnforcerRegistry
93
95
  +register(opts) this
96
+ +hasEnforcers() boolean
94
97
  +resolveEnforcer(opts) Promise
95
98
  +resolveOptions() IAuthorizeOptions
96
99
  }
@@ -196,6 +199,7 @@ auth/authorize/
196
199
  | **Abstract base** | `AbstractAuthRegistry<T>` shared between authentication and authorization registries |
197
200
  | **Filtered adapter pattern** | `BaseFilteredAdapter` template method pattern allows custom query backends while sharing formatting logic |
198
201
  | **IAuthorizationComparable** | Generic comparison interface for custom action/resource types beyond plain strings |
202
+ | **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 |
199
203
 
200
204
  ## Component Lifecycle
201
205
 
@@ -298,6 +302,7 @@ class AuthorizationEnforcerRegistry extends AbstractAuthRegistry<IAuthorizationE
298
302
  protected getBindingPrefix(): string; // returns Authorization.ENFORCER
299
303
 
300
304
  register(opts: { ... }): this;
305
+ hasEnforcers(): boolean;
301
306
  getDefaultEnforcerName(): string;
302
307
  resolveEnforcer(opts: { name: string }): Promise<IAuthorizationEnforcer>;
303
308
  resolveOptions(): IAuthorizeOptions | undefined;
@@ -310,6 +315,7 @@ class AuthorizationEnforcerRegistry extends AbstractAuthRegistry<IAuthorizationE
310
315
  |--------|---------|-------------|
311
316
  | `getInstance()` | `AuthorizationEnforcerRegistry` | Returns the singleton instance (creates on first call) |
312
317
  | `register(opts)` | `this` | Registers enforcers with type-safe options. See below. |
318
+ | `hasEnforcers()` | `boolean` | Returns `true` if any enforcers are registered (`descriptors.size > 0`). Used by the middleware to skip authorization when no enforcers exist. |
313
319
  | `getDefaultEnforcerName()` | `string` | Delegates to `getDefaultName()` -- returns the first registered enforcer's name |
314
320
  | `resolveEnforcer({ name })` | `Promise<IAuthorizationEnforcer>` | Resolves and auto-configures an enforcer (configure-once pattern via `configuredEnforcers` Set) |
315
321
  | `resolveOptions()` | `IAuthorizeOptions \| undefined` | Iterates all registered containers looking for `AuthorizeBindingKeys.OPTIONS` |
@@ -578,12 +584,12 @@ Called once by the registry on first use. Performs:
578
584
  1. Dynamically imports `casbin` -- throws `"casbin" is not installed` if missing
579
585
  2. Validates `options.model` -- throws `options.model is required.` if missing
580
586
  3. Resolves model via driver:
581
- - `'file'` `casbin.newModelFromFile(definition)`
582
- - `'text'` `casbin.newModelFromString(definition)`
587
+ - `'file'` -> `casbin.newModelFromFile(definition)`
588
+ - `'text'` -> `casbin.newModelFromString(definition)`
583
589
  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)
590
+ - `cached.use: false` -> `casbin.newEnforcer(model, adapter)`
591
+ - `cached.driver: 'in-memory'` -> `casbin.newCachedEnforcer(model, adapter)` + periodic invalidation timer (`setInterval`)
592
+ - `cached.driver: 'redis'` -> `casbin.newEnforcer(model, adapter)` (Redis handles caching externally)
587
593
  5. Validates `expiresIn >= MIN_EXPIRES_IN` (10,000 ms) for both in-memory and redis cache drivers
588
594
 
589
595
  ### destroy()
@@ -623,7 +629,7 @@ flowchart TD
623
629
  |-------------|----------|
624
630
  | `use: false` | Load policies from adapter directly via `loadPoliciesFromAdapter()` |
625
631
  | `'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 |
632
+ | `'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
633
 
628
634
  Returns the `IAuthUser` directly (Casbin evaluates policies from its internal model, not from the returned value).
629
635
 
@@ -652,7 +658,7 @@ Returns `AuthorizationDecisions.ALLOW` or `AuthorizationDecisions.DENY`.
652
658
  | `resolveModel` | `{ casbin, model }` | `Model` | Resolves casbin model from file or text via driver discriminant |
653
659
  | `validateExpiresIn` | `{ expiresIn }` | `void` | Throws if `expiresIn < MIN_EXPIRES_IN` |
654
660
  | `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 |
661
+ | `loadPoliciesWithRedisCache` | `{ user, cached }` | `void` | Redis cache flow: check cache -> hit: load lines; miss: load from adapter + cache |
656
662
  | `extractPolicyLines` | -- | `string[]` | Extracts `p` and `g` lines from enforcer model via `getPolicy()` and `getGroupingPolicy()` |
657
663
  | `loadPolicyLinesIntoModel` | `{ lines }` | `void` | Clears model, loads lines via `Helper.loadPolicyLine()`, rebuilds role links |
658
664
 
@@ -982,7 +988,8 @@ for (voter of spec.voters) {
982
988
  // ABSTAIN → continue to next voter
983
989
  }
984
990
 
985
- // Step 5: Resolve enforcer
991
+ // Step 5: Resolve enforcer (with no-enforcer fallback)
992
+ if (!registry.hasEnforcers()) → next() // skip if no enforcers registered
986
993
  const resolvedName = enforcerName ?? registry.getDefaultEnforcerName();
987
994
  const enforcer = await registry.resolveEnforcer({ name: resolvedName });
988
995
 
@@ -1114,10 +1121,14 @@ AuthorizationRoles.ADMIN.isEqualTo({ target: AuthorizationRoles.ADMIN });
1114
1121
 
1115
1122
  ### How Authorization Middleware is Injected
1116
1123
 
1117
- The `AbstractController.getRouteConfigs()` method handles middleware injection order:
1124
+ Authorization is supported in both **REST** and **gRPC** controllers.
1125
+
1126
+ #### REST Controllers
1127
+
1128
+ The `AbstractRestController.buildRouteMiddlewares()` method handles middleware injection order. `getRouteConfigs()` calls `buildRouteMiddlewares()` internally:
1118
1129
 
1119
1130
  ```typescript
1120
- getRouteConfigs<RouteConfig extends IAuthRouteConfig>(opts: { configs: RouteConfig }) {
1131
+ buildRouteMiddlewares<RouteConfig extends IAuthRouteConfig>(opts: { configs: RouteConfig }) {
1121
1132
  const { authenticate = {}, authorize, ...restConfig } = configs;
1122
1133
  const mws = [];
1123
1134
 
@@ -1137,13 +1148,38 @@ getRouteConfigs<RouteConfig extends IAuthRouteConfig>(opts: { configs: RouteConf
1137
1148
  // 3. Custom middleware (last)
1138
1149
  if (restConfig.middleware) { ... }
1139
1150
 
1140
- return createRoute({ ...restConfig, middleware: mws, ... });
1151
+ return { restConfig, security, mws };
1152
+ }
1153
+ ```
1154
+
1155
+ #### gRPC Controllers
1156
+
1157
+ The `AbstractGrpcController.buildRpcMiddlewares()` method provides symmetric middleware injection for gRPC:
1158
+
1159
+ ```typescript
1160
+ buildRpcMiddlewares(opts: { configs: IRpcMetadata }): TRpcMiddleware[] {
1161
+ const { configs } = opts;
1162
+ const mws = [];
1163
+
1164
+ // 1. Authenticate middleware (first)
1165
+ if (configs.authenticate) { ... }
1166
+
1167
+ // 2. Authorize middleware (second) — same pattern as REST
1168
+ if (configs.authorize) {
1169
+ const specs = Array.isArray(configs.authorize) ? configs.authorize : [configs.authorize];
1170
+ for (const spec of specs) {
1171
+ const authzMw = authorizeFn({ spec });
1172
+ mws.push((context, next) => authzMw(context, next));
1173
+ }
1174
+ }
1175
+
1176
+ return mws;
1141
1177
  }
1142
1178
  ```
1143
1179
 
1144
1180
  ### IAuthRouteConfig
1145
1181
 
1146
- Extended route config that supports both authentication and authorization:
1182
+ Extended route config that supports both authentication and authorization (REST):
1147
1183
 
1148
1184
  ```typescript
1149
1185
  interface IAuthRouteConfig extends HonoRouteConfig {
@@ -1154,6 +1190,19 @@ interface IAuthRouteConfig extends HonoRouteConfig {
1154
1190
 
1155
1191
  When `authorize` is an array, each spec creates a separate middleware. All must pass for the handler to execute.
1156
1192
 
1193
+ ### Per-Route Auth Types (CRUD Factory)
1194
+
1195
+ ```typescript
1196
+ /** Per-route authorization config: { skip: true }, single spec, or array of specs. */
1197
+ type TRouteAuthorizeConfig = { skip: true } | IAuthorizationSpec | IAuthorizationSpec[];
1198
+
1199
+ /** Per-route auth config. Endpoint config takes precedence over controller-level config. */
1200
+ type TRouteAuthConfig = {
1201
+ authenticate?: TRouteAuthenticateConfig;
1202
+ authorize?: TRouteAuthorizeConfig;
1203
+ };
1204
+ ```
1205
+
1157
1206
  ## IAuthUser Interface
1158
1207
 
1159
1208
  The user object available during authorization. Defined in `authenticate/common/types.ts`:
@@ -14,12 +14,13 @@ flowchart TD
14
14
  S3 -->|Match| OK2([No error - bypass])
15
15
  S3 -->|No match| S4{"Step 4: Voters"}
16
16
  S4 -->|DENY| E403a[/"403: Authorization denied by voter"/]
17
- S4 -->|ALLOW/ABSTAIN| S5{"Step 5: Resolve enforcer"}
17
+ S4 -->|ALLOW/ABSTAIN| S5{"Step 5: Enforcers registered?"}
18
18
 
19
- S5 -->|Registry empty| E500a[/"500: No items registered"/]
20
- S5 -->|Name not found| E500b[/"500: Descriptor not found"/]
21
- S5 -->|DI fails| E500c[/"500: Failed to resolve"/]
22
- S5 -->|OK| S6{"Step 6: Build rules"}
19
+ S5 -->|No enforcers| OK3([No error - skip])
20
+ S5 -->|Yes| S5b{"Resolve enforcer"}
21
+ S5b -->|Name not found| E500b[/"500: Descriptor not found"/]
22
+ S5b -->|DI fails| E500c[/"500: Failed to resolve"/]
23
+ S5b -->|OK| S6{"Step 6: Build rules"}
23
24
 
24
25
  S6 -->|No principalType| E400a[/"400: principalType required"/]
25
26
  S6 -->|Not initialized| E500d[/"500: Enforcer not initialized"/]
@@ -29,7 +30,7 @@ flowchart TD
29
30
  S6 -->|OK| S7{"Step 7: Evaluate"}
30
31
 
31
32
  S7 -->|No action/resource| E500g[/"500: request.action and resource required"/]
32
- S7 -->|ALLOW| OK3([Authorized])
33
+ S7 -->|ALLOW| OK4([Authorized])
33
34
  S7 -->|DENY| E403b[/"403: Authorization denied"/]
34
35
  ```
35
36
 
@@ -63,6 +64,9 @@ All error messages from the authorization module, organized by source:
63
64
  | <code v-pre>[AuthorizationEnforcerRegistry] Descriptor not found: {{name}}</code> | 500 | `resolveDescriptor` |
64
65
  | <code v-pre>[AuthorizationEnforcerRegistry] Failed to resolve: {{name}}</code> | 500 | `resolveDescriptor` |
65
66
 
67
+ > [!NOTE]
68
+ > The `[AuthorizationEnforcerRegistry] No items registered` error can only occur if `getDefaultEnforcerName()` is called directly. During normal middleware execution, the provider checks `registry.hasEnforcers()` first and skips authorization if no enforcers are registered, so this error is not thrown in the standard pipeline.
69
+
66
70
  ### Casbin Enforcer Errors (CasbinAuthorizationEnforcer)
67
71
 
68
72
  | Error Message | Status | Method |
@@ -198,7 +202,7 @@ AuthorizationEnforcerRegistry.getInstance().register({
198
202
 
199
203
  ### "[AuthorizationEnforcerRegistry] No items registered"
200
204
 
201
- **Cause:** Tried to get the default enforcer name but no enforcers are registered. This usually means `AuthorizationEnforcerRegistry.register()` was never called.
205
+ **Cause:** Tried to get the default enforcer name but no enforcers are registered. This error can only occur if `getDefaultEnforcerName()` is called directly. During normal middleware execution, the provider checks `registry.hasEnforcers()` first and skips authorization if no enforcers are registered, so this error is not triggered in the standard pipeline.
202
206
 
203
207
  **Fix:** Register enforcers after registering the component:
204
208
 
@@ -358,6 +362,7 @@ Check middleware injection order:
358
362
  1. Verify `authorize` is set on the route config (not just `authenticate`)
359
363
  2. Verify `authenticate` is not set to `{ skip: true }` (which also skips authorization in CRUD factory)
360
364
  3. Verify the component is registered and enforcers are registered via the registry
365
+ 4. Note: if no enforcers are registered, the middleware skips authorization silently (calls `next()`) rather than throwing an error
361
366
 
362
367
  ### Rules Are Built On Every Request
363
368
 
@@ -40,10 +40,12 @@ flowchart TD
40
40
  S3 -->|No match| S4{"4. Voters?"}
41
41
  S4 -->|DENY| E403a[/403/]
42
42
  S4 -->|ALLOW| Pass4([next])
43
- S4 -->|ABSTAIN| S5["5. Resolve enforcer"]
44
- S5 --> S6["6. Build/cache rules"]
43
+ S4 -->|ABSTAIN / none| S5{"5. Enforcers registered?"}
44
+ S5 -->|No enforcers| Pass5([next - skip])
45
+ S5 -->|Yes| S5b["Resolve enforcer"]
46
+ S5b --> S6["6. Build/cache rules"]
45
47
  S6 --> S7{"7. Evaluate"}
46
- S7 -->|ALLOW| Pass5([next])
48
+ S7 -->|ALLOW| Pass6([next])
47
49
  S7 -->|DENY/ABSTAIN| E403b[/403/]
48
50
  ```
49
51
 
@@ -53,13 +55,16 @@ flowchart TD
53
55
  | 2 | Get authenticated user from context | Yes -- 401 if missing |
54
56
  | 3 | Check role-based shortcuts (`alwaysAllowRoles` + `allowedRoles`) | Yes -- allow if matched |
55
57
  | 4 | Execute `voters` (per-route) | Yes -- DENY/ALLOW short-circuits |
56
- | 5 | Resolve enforcer (by name or default) | No |
58
+ | 5 | Check if enforcers are registered; resolve enforcer (by name or default) | Yes -- skip all if no enforcers registered |
57
59
  | 6 | Build or retrieve cached rules | No |
58
60
  | 7 | Evaluate permission via enforcer | Yes -- 403 if denied |
59
61
 
60
62
  > [!NOTE]
61
63
  > Step 3 merges the global `alwaysAllowRoles` check and the per-route `allowedRoles` check into a single step. User roles are extracted once and checked against both lists.
62
64
 
65
+ > [!NOTE]
66
+ > Step 5 has a safety fallback: if no enforcers are registered in the `AuthorizationEnforcerRegistry`, the middleware skips authorization entirely and calls `next()`. This prevents hard failures when authorization is configured on routes but no enforcer has been registered yet.
67
+
63
68
  ### Authorization Constants
64
69
 
65
70
  | Constant | Value | Description |
@@ -645,7 +650,7 @@ interface IJWTTokenPayload extends JWTPayload, IAuthUser {
645
650
 
646
651
  ## Relationship with Authentication
647
652
 
648
- Authorization runs **after** authentication in the middleware chain. The `AbstractController.getRouteConfigs()` method ensures the correct ordering:
653
+ Authorization runs **after** authentication in the middleware chain. Both REST and gRPC controllers ensure the correct ordering:
649
654
 
650
655
  ```mermaid
651
656
  flowchart LR
@@ -657,10 +662,79 @@ flowchart LR
657
662
 
658
663
  1. Authentication middleware is injected first (from `authenticate` config)
659
664
  2. Authorization middleware is injected second (from `authorize` config)
660
- 3. Custom middleware is injected last (from `middleware` config)
665
+ 3. Custom middleware is injected last (from `middleware` config -- REST only)
661
666
 
662
667
  This means `Authentication.CURRENT_USER` is always available when the authorization middleware executes.
663
668
 
669
+ ### REST Controllers
670
+
671
+ The `AbstractRestController.buildRouteMiddlewares()` method builds the middleware array from route config. The `getRouteConfigs()` method calls `buildRouteMiddlewares()` internally and wraps it into a Hono route definition:
672
+
673
+ ```typescript
674
+ // In AbstractRestController
675
+ buildRouteMiddlewares<RouteConfig extends IAuthRouteConfig>(opts: { configs: RouteConfig }) {
676
+ const { authenticate = {}, authorize, ...restConfig } = opts.configs;
677
+ const mws = [];
678
+
679
+ // 1. Authenticate middleware (first)
680
+ if (strategies.length > 0) {
681
+ mws.push(authenticateFn({ strategies, mode }));
682
+ }
683
+
684
+ // 2. Authorize middleware (second) — supports single or array
685
+ if (authorize) {
686
+ const specs = Array.isArray(authorize) ? authorize : [authorize];
687
+ for (const spec of specs) {
688
+ mws.push(authorizeFn({ spec }));
689
+ }
690
+ }
691
+
692
+ // 3. Custom middleware (last)
693
+ if (restConfig.middleware) { ... }
694
+
695
+ return { restConfig, security, mws };
696
+ }
697
+ ```
698
+
699
+ ### gRPC Controllers
700
+
701
+ The `AbstractGrpcController.buildRpcMiddlewares()` method provides symmetric authorization support for gRPC routes. Authorization specs are applied the same way as REST:
702
+
703
+ ```typescript
704
+ // In AbstractGrpcController
705
+ buildRpcMiddlewares(opts: { configs: IRpcMetadata }): TRpcMiddleware[] {
706
+ const { configs } = opts;
707
+ const mws = [];
708
+
709
+ // 1. Authenticate middleware
710
+ if (configs.authenticate) { ... }
711
+
712
+ // 2. Authorize middleware — same pattern as REST
713
+ if (configs.authorize) {
714
+ const specs = Array.isArray(configs.authorize) ? configs.authorize : [configs.authorize];
715
+ for (const spec of specs) {
716
+ const authzMw = authorizeFn({ spec });
717
+ mws.push((context, next) => authzMw(context, next));
718
+ }
719
+ }
720
+
721
+ return mws;
722
+ }
723
+ ```
724
+
725
+ ### IAuthRouteConfig
726
+
727
+ Extended route config that supports both authentication and authorization:
728
+
729
+ ```typescript
730
+ interface IAuthRouteConfig extends HonoRouteConfig {
731
+ authenticate?: { strategies?: TAuthStrategy[]; mode?: TAuthMode };
732
+ authorize?: IAuthorizationSpec | IAuthorizationSpec[];
733
+ }
734
+ ```
735
+
736
+ When `authorize` is an array, each spec creates a separate middleware. All must pass for the handler to execute.
737
+
664
738
  ### Per-Route Configuration in CRUD Factory
665
739
 
666
740
  CRUD factory routes support both authentication and authorization configuration:
@@ -694,6 +768,19 @@ ControllerFactory.defineCrudController({
694
768
  3. Per-route `authorize` overrides controller-level `authorize`
695
769
  4. No per-route config -- inherits controller-level config
696
770
 
771
+ ### Per-Route Auth Types
772
+
773
+ ```typescript
774
+ /** Per-route authorization config: { skip: true }, single spec, or array of specs. */
775
+ type TRouteAuthorizeConfig = { skip: true } | IAuthorizationSpec | IAuthorizationSpec[];
776
+
777
+ /** Per-route auth config. Endpoint config takes precedence over controller-level config. */
778
+ type TRouteAuthConfig = {
779
+ authenticate?: TRouteAuthenticateConfig;
780
+ authorize?: TRouteAuthorizeConfig;
781
+ };
782
+ ```
783
+
697
784
  ## See Also
698
785
 
699
786
  - [Usage & Examples](./usage) -- Securing routes, voters, patterns, and CRUD integration
@@ -10,12 +10,12 @@ Use the `authorize` field in route configs to declare authorization requirements
10
10
 
11
11
  ```typescript
12
12
  import {
13
- BaseController,
13
+ BaseRestController,
14
14
  Authentication,
15
15
  AuthorizationActions,
16
16
  } from '@venizia/ignis';
17
17
 
18
- class ArticleController extends BaseController {
18
+ class ArticleController extends BaseRestController {
19
19
  binding() {
20
20
  // Read requires 'read' action on 'Article' resource
21
21
  this.defineRoute({
@@ -102,7 +102,7 @@ Use the `authorize` field alongside `authenticate` in decorator configs:
102
102
  import { controller, get, post, AuthorizationActions, AuthorizationRoles } from '@venizia/ignis';
103
103
 
104
104
  @controller({ path: '/articles' })
105
- class ArticleController extends BaseController {
105
+ class ArticleController extends BaseRestController {
106
106
  @get({
107
107
  configs: {
108
108
  path: '/',
@@ -134,6 +134,34 @@ class ArticleController extends BaseController {
134
134
  }
135
135
  ```
136
136
 
137
+ ### gRPC Route Authorization
138
+
139
+ Authorization works the same way in gRPC controllers. Use the `authorize` field in RPC metadata:
140
+
141
+ ```typescript
142
+ import { AuthorizationActions, Authentication } from '@venizia/ignis';
143
+
144
+ class GreeterController extends BaseGrpcController {
145
+ binding() {
146
+ this.defineRoute({
147
+ configs: {
148
+ method: sayHello,
149
+ authenticate: { strategies: [Authentication.STRATEGY_JWT] },
150
+ authorize: {
151
+ action: AuthorizationActions.EXECUTE,
152
+ resource: 'Greeter',
153
+ },
154
+ },
155
+ handler: async (context) => {
156
+ // Handler runs only if authorized
157
+ },
158
+ });
159
+ }
160
+ }
161
+ ```
162
+
163
+ The `AbstractGrpcController.buildRpcMiddlewares()` method injects authorization middleware in the same order as REST controllers: authenticate first, then authorize.
164
+
137
165
  ## Using the `authorize()` Standalone Function
138
166
 
139
167
  The `authorize()` function is a convenience wrapper around `AuthorizationProvider`. It returns a Hono `MiddlewareHandler`:
@@ -383,7 +411,7 @@ ControllerFactory.defineCrudController({
383
411
 
384
412
  ### Priority Resolution (Factory Routes)
385
413
 
386
- The `defineControllerRouteConfigs` function resolves authorization with this priority:
414
+ The `resolveRouteAuthorize` function in `defineControllerRouteConfigs` resolves authorization with this priority:
387
415
 
388
416
  ```mermaid
389
417
  flowchart TD
@@ -403,6 +431,16 @@ flowchart TD
403
431
  3. **Per-route `authorize` spec** -- overrides controller-level
404
432
  4. **Controller-level `authorize`** -- default for all routes
405
433
 
434
+ ### Per-Route Auth Type
435
+
436
+ The per-route authorize config is typed as a discriminated union:
437
+
438
+ ```typescript
439
+ type TRouteAuthorizeConfig = { skip: true } | IAuthorizationSpec | IAuthorizationSpec[];
440
+ ```
441
+
442
+ This means each route can either skip authorization entirely, provide a single spec, or provide an array of specs that all must pass.
443
+
406
444
  ## Dynamic Skip Authorization
407
445
 
408
446
  Use `Authorization.SKIP_AUTHORIZATION` to dynamically bypass authorization in middleware (step 1 in the pipeline):
@@ -213,10 +213,11 @@ The controller uses `this.definitions = RouteConfigs` to store route configurati
213
213
  #### Controller Source
214
214
  ```typescript
215
215
  import {
216
- BaseController, IControllerOptions, TRouteContext,
217
- api, jsonContent, jsonResponse, z,
216
+ BaseRestController, IControllerOptions, TRouteContext,
218
217
  } from '@venizia/ignis';
219
- import { HTTP } from '@venizia/ignis-helpers';
218
+ import { api, jsonContent, jsonResponse } from '@venizia/ignis';
219
+ import { z } from '@hono/zod-openapi';
220
+ import { HTTP, ValueOrPromise } from '@venizia/ignis-helpers';
220
221
 
221
222
  const RouteConfigs = {
222
223
  ROOT: {
@@ -258,7 +259,7 @@ const RouteConfigs = {
258
259
  },
259
260
  } as const;
260
261
 
261
- export class HealthCheckController extends BaseController {
262
+ export class HealthCheckController extends BaseRestController {
262
263
  constructor(opts: IControllerOptions) {
263
264
  super({ ...opts, scope: HealthCheckController.name });
264
265
  this.definitions = RouteConfigs;
@@ -15,6 +15,7 @@ Reusable, pluggable modules that group together related features. A component ca
15
15
  | [WebSocket](./websocket/) | Real-time communication | Bun native WebSocket, Redis Pub/Sub, heartbeat |
16
16
  | [Static Asset](./static-asset/) | File management | Upload/download files, MinIO & local filesystem support |
17
17
  | [Swagger](./swagger) | API documentation | OpenAPI generation, Swagger UI, Scalar UI |
18
+ | [gRPC](/references/base/grpc-controllers) | gRPC transport | ConnectRPC integration, unary RPC, decorator-based |
18
19
 
19
20
  ## Creating a Component
20
21
 
@@ -81,6 +82,7 @@ Using components is a great way to organize your application's features into mod
81
82
  - [WebSocket](./websocket/) - Bun native WebSocket
82
83
  - [Static Asset](./static-asset/) - Static file serving
83
84
  - [Swagger](./swagger) - API documentation
85
+ - [gRPC](/references/base/grpc-controllers) - gRPC transport (ConnectRPC)
84
86
 
85
87
  - **References:**
86
88
  - [BaseComponent API](/references/base/components) - Component base class
@@ -211,7 +211,7 @@ The `TMailOptions` configuration determines which email transport provider is us
211
211
 
212
212
  ```typescript
213
213
  // Simple SMTP Authentication (e.g., Gmail with app password)
214
- this.bind<TMailOptions>({ key: MailBindingKeys.MAIL_OPTIONS }).toValue({
214
+ this.bind<TMailOptions>({ key: MailKeys.MAIL_OPTIONS }).toValue({
215
215
  provider: 'nodemailer',
216
216
  from: 'noreply@example.com',
217
217
  fromName: 'My App',
@@ -248,7 +248,7 @@ If running without a proxy, ensure the runtime provides connection info (Bun doe
248
248
  - [All Components](./index) - Built-in components list
249
249
 
250
250
  - **Helpers:**
251
- - [Logger Helper](/references/helpers/logger/) - Logging utilities
251
+ - [Logger Helper](/extensions/helpers/logger/) - Logging utilities
252
252
 
253
253
  - **Best Practices:**
254
254
  - [Troubleshooting Tips](/best-practices/troubleshooting-tips) - Debugging with request IDs
@@ -630,7 +630,7 @@ Reads all binding keys from the DI container and validates required ones:
630
630
  |---------|-----------|------------------|
631
631
  | `SERVER_OPTIONS` | Optional, merged with defaults via `Object.assign()` | -- |
632
632
  | `REDIS_CONNECTION` | Must be `instanceof DefaultRedisHelper` | `"Invalid instance of redisConnection | Please init connection with RedisHelper for single redis connection or RedisClusterHelper for redis cluster mode!"` |
633
- | `AUTHENTICATE_HANDLER` | Must be a function (non-null) | `"Invalid authenticateFn to setup io socket server!"` |
633
+ | `AUTHENTICATE_HANDLER` | Must be a function (non-null) | `"[DANGER][SocketIOComponent] Invalid authenticateFn to setup io socket server!"` |
634
634
  | `VALIDATE_ROOM_HANDLER` | Optional, resolved from container, `null` coerced to `undefined` | -- |
635
635
  | `CLIENT_CONNECTED_HANDLER` | Optional, resolved from container, `null` coerced to `undefined` | -- |
636
636
 
@@ -673,7 +673,7 @@ Returns a fetch handler function that routes requests:
673
673
  Registers a post-start hook that:
674
674
 
675
675
  1. Gets the HTTP server instance via `getServerInstance()`
676
- 2. Validates the server instance exists (throws `"HTTP server not available for Node.js runtime!"` if not)
676
+ 2. Validates the server instance exists (throws `"[SocketIOComponent] HTTP server not available for Node.js runtime!"` if not)
677
677
  3. Calls `createNodeSocketIOHelper()` which creates `SocketIOServerHelper` with `runtime: RuntimeModules.NODE` and the HTTP server, then awaits `configure()`
678
678
  4. Binds the helper to `SOCKET_IO_INSTANCE`
679
679
 
@@ -59,6 +59,8 @@
59
59
  **Fix**: Use `RedisHelper` (single instance) or `RedisClusterHelper` (cluster mode):
60
60
 
61
61
  ```typescript
62
+ import { SocketIOBindingKeys } from '@venizia/ignis/socket-io';
63
+
62
64
  // Correct -- single instance
63
65
  this.bind({ key: SocketIOBindingKeys.REDIS_CONNECTION })
64
66
  .toValue(new RedisHelper({ name: 'socket-io', host, port, password }));