@venizia/ignis-docs 0.2.0 → 0.2.1-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/README.md +14 -14
  2. package/content/best-practices/api-usage-examples.md +39 -19
  3. package/content/best-practices/architectural-patterns.md +22 -11
  4. package/content/best-practices/architecture-decisions.md +29 -16
  5. package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
  6. package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
  7. package/content/best-practices/code-style-standards/control-flow.md +33 -1
  8. package/content/best-practices/code-style-standards/documentation.md +28 -8
  9. package/content/best-practices/code-style-standards/function-patterns.md +15 -4
  10. package/content/best-practices/code-style-standards/index.md +7 -2
  11. package/content/best-practices/code-style-standards/naming-conventions.md +26 -2
  12. package/content/best-practices/code-style-standards/route-definitions.md +9 -5
  13. package/content/best-practices/code-style-standards/tooling.md +14 -1
  14. package/content/best-practices/code-style-standards/type-safety.md +39 -6
  15. package/content/best-practices/common-pitfalls.md +59 -35
  16. package/content/best-practices/contribution-workflow.md +6 -2
  17. package/content/best-practices/data-modeling.md +78 -62
  18. package/content/best-practices/deployment-strategies.md +56 -19
  19. package/content/best-practices/error-handling.md +182 -93
  20. package/content/best-practices/index.md +6 -0
  21. package/content/best-practices/performance-optimization.md +26 -13
  22. package/content/best-practices/security-guidelines.md +35 -9
  23. package/content/best-practices/testing-strategies.md +7 -2
  24. package/content/best-practices/troubleshooting-tips.md +27 -17
  25. package/content/extensions/components/api-reference.md +107 -322
  26. package/content/extensions/components/authentication/api.md +454 -603
  27. package/content/extensions/components/authentication/errors.md +121 -498
  28. package/content/extensions/components/authentication/index.md +88 -801
  29. package/content/extensions/components/authentication/usage.md +207 -956
  30. package/content/extensions/components/authorization/api.md +736 -656
  31. package/content/extensions/components/authorization/errors.md +168 -206
  32. package/content/extensions/components/authorization/index.md +82 -797
  33. package/content/extensions/components/authorization/usage.md +194 -527
  34. package/content/extensions/components/health-check.md +71 -243
  35. package/content/extensions/components/mail/api.md +504 -287
  36. package/content/extensions/components/mail/errors.md +73 -61
  37. package/content/extensions/components/mail/index.md +96 -467
  38. package/content/extensions/components/mail/usage.md +130 -172
  39. package/content/extensions/components/request-tracker.md +66 -173
  40. package/content/extensions/components/socket-io/api.md +195 -13
  41. package/content/extensions/components/socket-io/errors.md +3 -3
  42. package/content/extensions/components/socket-io/index.md +50 -337
  43. package/content/extensions/components/socket-io/usage.md +143 -26
  44. package/content/extensions/components/static-asset/api.md +410 -142
  45. package/content/extensions/components/static-asset/errors.md +110 -53
  46. package/content/extensions/components/static-asset/index.md +79 -608
  47. package/content/extensions/components/static-asset/usage.md +180 -300
  48. package/content/extensions/components/websocket/api.md +275 -399
  49. package/content/extensions/components/websocket/errors.md +47 -56
  50. package/content/extensions/components/websocket/index.md +74 -407
  51. package/content/extensions/components/websocket/usage.md +110 -341
  52. package/content/extensions/helpers/cron/index.md +51 -160
  53. package/content/extensions/helpers/crypto/index.md +62 -483
  54. package/content/extensions/helpers/crypto/reference.md +456 -0
  55. package/content/extensions/helpers/env/index.md +60 -178
  56. package/content/extensions/helpers/error/index.md +221 -207
  57. package/content/extensions/helpers/inversion/index.md +65 -556
  58. package/content/extensions/helpers/inversion/reference.md +522 -0
  59. package/content/extensions/helpers/kafka/admin.md +20 -1
  60. package/content/extensions/helpers/kafka/compile-binary.md +41 -35
  61. package/content/extensions/helpers/kafka/consumer.md +54 -20
  62. package/content/extensions/helpers/kafka/examples.md +21 -16
  63. package/content/extensions/helpers/kafka/index.md +80 -610
  64. package/content/extensions/helpers/kafka/producer.md +134 -5
  65. package/content/extensions/helpers/kafka/schema-registry.md +45 -70
  66. package/content/extensions/helpers/logger/hf-logger.md +193 -0
  67. package/content/extensions/helpers/logger/index.md +64 -563
  68. package/content/extensions/helpers/logger/pino.md +85 -0
  69. package/content/extensions/helpers/logger/reference.md +746 -0
  70. package/content/extensions/helpers/network/api.md +241 -195
  71. package/content/extensions/helpers/network/index.md +72 -530
  72. package/content/extensions/helpers/queue/index.md +72 -900
  73. package/content/extensions/helpers/queue/reference.md +467 -0
  74. package/content/extensions/helpers/redis/index.md +73 -645
  75. package/content/extensions/helpers/redis/reference.md +727 -0
  76. package/content/extensions/helpers/secrets/index.md +66 -0
  77. package/content/extensions/helpers/socket-io/api.md +305 -203
  78. package/content/extensions/helpers/socket-io/index.md +66 -432
  79. package/content/extensions/helpers/storage/api.md +564 -462
  80. package/content/extensions/helpers/storage/index.md +77 -573
  81. package/content/extensions/helpers/types/index.md +66 -499
  82. package/content/extensions/helpers/types/reference.md +650 -0
  83. package/content/extensions/helpers/uid/index.md +58 -227
  84. package/content/extensions/helpers/websocket/api.md +329 -216
  85. package/content/extensions/helpers/websocket/index.md +65 -503
  86. package/content/extensions/helpers/worker-thread/index.md +58 -396
  87. package/content/extensions/helpers/worker-thread/reference.md +428 -0
  88. package/content/guides/core-concepts/persistent/models.md +1 -1
  89. package/content/guides/core-concepts/persistent/search-typesense.md +3 -3
  90. package/content/guides/core-concepts/persistent/transactions.md +1 -1
  91. package/content/guides/core-concepts/secrets-vault.md +177 -0
  92. package/content/guides/core-concepts/services.md +1 -1
  93. package/content/guides/migrations/redis-helpers-migration.md +1 -1
  94. package/content/guides/migrations/unified-connectors-migration.md +2 -2
  95. package/content/guides/tutorials/ecommerce-api.md +3 -8
  96. package/content/references/base/application.md +1 -1
  97. package/content/references/base/connectors.md +79 -136
  98. package/content/references/base/datasources-reference.md +599 -0
  99. package/content/references/base/datasources.md +84 -444
  100. package/content/references/base/dependency-injection.md +17 -39
  101. package/content/references/base/filter-system/application-usage.md +69 -121
  102. package/content/references/base/filter-system/array-operators.md +12 -0
  103. package/content/references/base/filter-system/comparison-operators.md +12 -0
  104. package/content/references/base/filter-system/default-filter.md +136 -348
  105. package/content/references/base/filter-system/fields-order-pagination.md +38 -16
  106. package/content/references/base/filter-system/index.md +106 -257
  107. package/content/references/base/filter-system/json-filtering.md +12 -2
  108. package/content/references/base/filter-system/list-operators.md +16 -2
  109. package/content/references/base/filter-system/logical-operators.md +13 -0
  110. package/content/references/base/filter-system/null-operators.md +13 -0
  111. package/content/references/base/filter-system/pattern-matching.md +12 -0
  112. package/content/references/base/filter-system/quick-reference.md +11 -2
  113. package/content/references/base/filter-system/range-operators.md +12 -0
  114. package/content/references/base/filter-system/tips.md +70 -133
  115. package/content/references/base/filter-system/use-cases.md +156 -233
  116. package/content/references/base/middlewares.md +35 -21
  117. package/content/references/base/models-reference.md +886 -0
  118. package/content/references/base/models.md +80 -1452
  119. package/content/references/base/repositories/advanced.md +156 -192
  120. package/content/references/base/repositories/index.md +77 -650
  121. package/content/references/base/repositories/mixins.md +22 -18
  122. package/content/references/base/repositories/relations.md +123 -171
  123. package/content/references/base/repositories/soft-deletable.md +58 -56
  124. package/content/references/base/secrets.md +263 -0
  125. package/content/references/base/services.md +2 -2
  126. package/content/references/configuration/environment-variables.md +48 -4
  127. package/content/references/configuration/index.md +49 -31
  128. package/content/references/quick-reference.md +3 -16
  129. package/content/references/utilities/crypto.md +35 -76
  130. package/content/references/utilities/date.md +33 -73
  131. package/content/references/utilities/index.md +1 -1
  132. package/content/references/utilities/jsx-reference.md +298 -0
  133. package/content/references/utilities/jsx.md +82 -525
  134. package/content/references/utilities/module.md +29 -62
  135. package/content/references/utilities/parse.md +34 -64
  136. package/content/references/utilities/performance.md +33 -58
  137. package/content/references/utilities/promise.md +28 -62
  138. package/content/references/utilities/request.md +57 -218
  139. package/content/references/utilities/schema.md +43 -137
  140. package/content/references/utilities/statuses-reference.md +361 -0
  141. package/content/references/utilities/statuses.md +63 -667
  142. package/package.json +8 -8
@@ -18,91 +18,97 @@ flowchart TD
18
18
 
19
19
  S5 -->|No enforcers| OK3([No error - skip])
20
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"/]
21
+ S5b -->|Name not found| E400b[/"400: Descriptor not found"/]
22
+ S5b -->|DI fails| E400c[/"400: Failed to resolve"/]
23
23
  S5b -->|OK| S6{"Step 6: Build rules"}
24
24
 
25
- S6 -->|Not configured| E500d[/"500: Not configured. Call configure() first."/]
26
- S6 -->|No FilteredAdapter| E500e[/"500: Adapter does not support loadFilteredPolicy"/]
27
- S6 -->|Empty Redis key| E400b[/"400: keyFn returned an empty cache key"/]
25
+ S6 -->|Not configured| E400d[/"400: Not configured. Call configure() first."/]
26
+ S6 -->|No FilteredAdapter| E400e[/"400: Adapter does not support loadFilteredPolicy"/]
27
+ S6 -->|Empty Redis key| E400f[/"400: keyFn returned an empty cache key"/]
28
+ S6 -->|No principalType| E400g[/"400: principalType is required"/]
28
29
  S6 -->|OK| S7{"Step 7: Evaluate"}
29
30
 
30
- S7 -->|No action/resource| E500g[/"500: request.action and resource required"/]
31
+ S7 -->|No action/resource| E400h[/"400: action and resource required"/]
31
32
  S7 -->|ALLOW| OK4([Authorized])
32
- S7 -->|DENY| E403b[/"403: Authorization denied"/]
33
+ S7 -->|DENY / ABSTAIN-as-deny| E403b[/"403: Authorization denied"/]
34
+
35
+ style E401 fill:#f8d7da,stroke:#dc3545
36
+ style E403a fill:#f8d7da,stroke:#dc3545
37
+ style E403b fill:#f8d7da,stroke:#dc3545
38
+ style E400b fill:#fff3cd,stroke:#ffc107
39
+ style E400c fill:#fff3cd,stroke:#ffc107
40
+ style E400d fill:#fff3cd,stroke:#ffc107
41
+ style E400e fill:#fff3cd,stroke:#ffc107
42
+ style E400f fill:#fff3cd,stroke:#ffc107
43
+ style E400g fill:#fff3cd,stroke:#ffc107
44
+ style E400h fill:#fff3cd,stroke:#ffc107
33
45
  ```
34
46
 
35
47
  ## Complete Error Reference
36
48
 
37
- All error messages from the authorization module, organized by source:
49
+ All error messages from the authorization module, organized by source. **Status** is what `error.statusCode` holds - most calls use `getError({ message })` with no explicit `statusCode`, which the shared `ApplicationError` defaults to **400**. Only the request-pipeline errors below (steps 2, 4, 7) set an explicit `401`/`403`.
38
50
 
39
- ### Component Errors (AuthorizeComponent)
51
+ ### Component Errors (`AuthorizeComponent`)
40
52
 
41
- | Error Message | Status | Method |
42
- |---------------|--------|--------|
43
- | `[AuthorizeComponent] No authorize options found. Bind options to AuthorizeBindingKeys.OPTIONS before registering the component.` | 500 | `binding` |
53
+ Thrown during `binding()`, at application startup.
44
54
 
45
- ### Authorization Provider Errors
55
+ | Error Message | Status | Method | Cause |
56
+ |---------------|--------|--------|-------|
57
+ | `[AuthorizeComponent] No authorize options found. Bind options to AuthorizeBindingKeys.OPTIONS before registering the component.` | 400 | `binding` | `IAuthorizeOptions` was never bound before `this.component(AuthorizeComponent)` |
46
58
 
47
- | Error Message | Status | Step |
48
- |---------------|--------|------|
49
- | `Authorization failed: No authenticated user found` | 401 | Step 2 -- User check |
50
- | `Authorization failed: user.principalType is required for enforcer-based authorization` | 400 | Step 6 -- Build rules |
51
- | <code v-pre>Authorization denied by voter &#124; action: {{action}} &#124; resource: {{resource}}</code> | 403 | Step 4 -- Voter DENY |
52
- | <code v-pre>Authorization denied &#124; action: {{action}} &#124; resource: {{resource}}</code> | 403 | Step 7 -- Enforcer denied |
59
+ ### Enforcer Registry Errors (`AuthorizationEnforcerRegistry` + inherited `AbstractAuthRegistry`)
53
60
 
54
- ### Enforcer Registry Errors (AuthorizationEnforcerRegistry)
61
+ | Error Message | Status | Method | Cause |
62
+ |---------------|--------|--------|-------|
63
+ | <code v-pre>[getKey] Invalid name &#124; name: {{name}}</code> | 400 | `getKey` | Enforcer name is empty or falsy |
64
+ | `[AuthorizationEnforcerRegistry] No items registered` | 400 | `getDefaultName` | `getDefaultEnforcerName()` called with zero enforcers registered |
65
+ | <code v-pre>[AuthorizationEnforcerRegistry] Duplicate enforcer name(s): {{names}}</code> | 400 | `register` | Two or more enforcers in the same `register()` call share a name |
66
+ | <code v-pre>[AuthorizationEnforcerRegistry] Enforcer already registered: {{name}}</code> | 400 | `register` | An enforcer with this name was already registered in a previous call |
67
+ | <code v-pre>[AuthorizationEnforcerRegistry] Descriptor not found: {{name}}</code> | 400 | `resolveDescriptor` | `enforcerName` doesn't match any registered enforcer |
68
+ | <code v-pre>[AuthorizationEnforcerRegistry] Failed to resolve: {{name}}</code> | 400 | `resolveDescriptor` | The registered class has unsatisfied `@inject` dependencies |
69
+ | <code v-pre>[AuthorizationEnforcerRegistry] Enforcer "{{name}}" does not support cache invalidation</code> | 400 | `invalidateUserCache` / `rebuildUserCache` | The resolved enforcer doesn't implement the optional cache-management methods |
55
70
 
56
- | Error Message | Status | Method |
57
- |---------------|--------|--------|
58
- | <code v-pre>[getKey] Invalid name &#124; name: {{name}}</code> | 500 | `getKey` |
59
- | `[AuthorizationEnforcerRegistry] No items registered` | 500 | `getDefaultName` |
60
- | <code v-pre>[AuthorizationEnforcerRegistry] Duplicate enforcer name(s): {{names}}</code> | 500 | `register` |
61
- | <code v-pre>[AuthorizationEnforcerRegistry] Enforcer already registered: {{name}}</code> | 500 | `register` |
62
- | <code v-pre>[AuthorizationEnforcerRegistry] Descriptor not found: {{name}}</code> | 500 | `resolveDescriptor` |
63
- | <code v-pre>[AuthorizationEnforcerRegistry] Failed to resolve: {{name}}</code> | 500 | `resolveDescriptor` |
71
+ > [!NOTE]
72
+ > `[AuthorizationEnforcerRegistry] No items registered` can only surface if `getDefaultEnforcerName()` is called directly. During normal middleware execution, the provider checks `registry.hasEnforcers()` first and skips authorization when no enforcers exist - it never reaches this throw.
73
+
74
+ ### Authorization Provider Errors (`AuthorizationProvider` - the request pipeline)
75
+
76
+ The only errors in the module with an **explicit** `statusCode`.
77
+
78
+ | Error Message | Status | Pipeline step | Cause |
79
+ |---------------|--------|------|-------|
80
+ | `Authorization failed: No authenticated user found` | 401 | 2 - User check | `Authentication.CURRENT_USER` is missing from the Hono context |
81
+ | <code v-pre>Authorization denied by voter &#124; action: {{action}} &#124; resource: {{resource}}</code> | 403 | 4 - Voters | A voter function returned `AuthorizationDecisions.DENY` |
82
+ | `Authorization failed: user.principalType is required for enforcer-based authorization` | 400 | 6 - Build rules | The authenticated user object has no `principalType` field |
83
+ | <code v-pre>Authorization denied &#124; action: {{action}} &#124; resource: {{resource}}</code> | 403 | 7 - Evaluate | The enforcer returned `DENY`, or `ABSTAIN` and `defaultDecision` resolved to `'deny'` |
84
+
85
+ ### Casbin Enforcer Errors - Startup (`CasbinAuthorizationEnforcer.configure()`)
86
+
87
+ | Error Message | Status | Method | Cause |
88
+ |---------------|--------|--------|-------|
89
+ | `[CasbinAuthorizationEnforcer] "casbin" is not installed` | 400 | `configure` | The optional `casbin` peer dependency isn't installed |
90
+ | `[CasbinAuthorizationEnforcer] options.model is required.` | 400 | `configure` | `model` is missing from the enforcer options |
91
+ | <code v-pre>[registerMatchers] Role definition "{{name}}" is not declared in the Casbin model. Declare it under [role_definition] (e.g. `g = _, _, _`) before enabling domainMatching.</code> | 400 | `configure` (via `registerMatchers`) | `domainMatching.roleDefinition` isn't declared in `[role_definition]` (only checked when `domainMatching` is set) |
92
+ | <code v-pre>[CasbinAuthorizationEnforcer] Matcher smoke test failed at warmup - the model matcher did not compile ... {{error}}</code> | 400 | `configure` (via `assertMatcherCompilesSync`) | The matcher expression doesn't compile: syntax error, an unregistered function, or an arity mismatch |
93
+ | <code v-pre>[CasbinAuthorizationEnforcer] cached.options.expiresIn must be >= 10000 (ms) &#124; Received: {{value}}</code> | 400 | `configure` (via `validateExpiresIn`) | `cached.options.expiresIn` is below `MIN_EXPIRES_IN` (10,000 ms) |
94
+ | <code v-pre>[resolveDomainMatchingFn] Unsupported func: {{name}} &#124; Valids: [...]</code> | 400 | `configure` (via `registerMatchers`) | `domainMatching.fn` isn't a `CasbinDomainMatchingFunctions` value |
95
+ | <code v-pre>[resolveModel] Invalid model.driver &#124; Valids: [file, text]</code> | 400 | `configure` (via `resolveModel`) | `model.driver` isn't `'file'` or `'text'` |
64
96
 
65
97
  > [!NOTE]
66
- > 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.
67
-
68
- ### Casbin Enforcer Errors (CasbinAuthorizationEnforcer)
69
-
70
- | Error Message | Status | Method |
71
- |---------------|--------|--------|
72
- | `[CasbinAuthorizationEnforcer] "casbin" is not installed` | 500 | `configure` |
73
- | `[CasbinAuthorizationEnforcer] options.model is required.` | 500 | `configure` |
74
- | `[CasbinAuthorizationEnforcer] Not configured. Call configure() first.` | 500 | `evaluate`, `loadPolicyLinesIntoModel` |
75
- | `[extractUserLines] Adapter does not support loadFilteredPolicy.` | 500 | `buildRules` (via `extractUserLines`) |
76
- | `[CasbinAuthorizationEnforcer] request.action and request.resource are required.` | 500 | `evaluate` |
77
- | `[CasbinAuthorizationEnforcer] keyFn returned an empty cache key.` | 400 | `buildRules`/cache management (via `resolveCacheKey`) |
78
- | <code v-pre>[CasbinAuthorizationEnforcer] cached.options.expiresIn must be >= 10000 (ms) &#124; Received: {{value}}</code> | 500 | `configure` (via `validateExpiresIn`) |
79
- | <code v-pre>[CasbinAuthorizationEnforcer] Matcher smoke test failed at warmup - ...</code> | 500 | `configure` (via `assertMatcherCompilesSync`) |
80
- | <code v-pre>[resolveDomainMatchingFn] Unsupported func: {{name}} &#124; Valids: [...]</code> | 500 | `configure` (via `registerMatchers`) |
81
- | <code v-pre>[registerMatchers] Role definition "{{name}}" is not declared in the Casbin model. ...</code> | 500 | `configure` (via `registerMatchers`, only when `domainMatching` is set) |
82
- | <code v-pre>[resolveModel] Invalid model.driver &#124; Valids: [file, text]</code> | 500 | `configure` (via `resolveModel`) |
83
- | `[CasbinAuthorizationEnforcer] Cache management requires the redis cache driver, but caching is disabled.` | 500 | `invalidateUserCache`/`rebuildUserCache` (via `requireRedisCache`) |
84
-
85
- > The `Invalid cached.driver` errors were removed - `cached` is now a typed union
86
- > (`{ use: false } | { use: true, driver: 'redis', ... }`), so an invalid driver is a compile-time
87
- > error, not a runtime one.
88
-
89
- ### Policy Loading Errors (CasbinAuthorizationEnforcer internals)
90
-
91
- | Error Message | Status | Method |
92
- |---------------|--------|--------|
93
- | `[CasbinAuthorizationEnforcer] keyFn returned an empty cache key.` | 400 | `resolveCacheKey` (read + cache-management paths) |
94
- | `[extractUserLines] Adapter does not support loadFilteredPolicy.` | 500 | `extractUserLines` |
95
- | `[loadPolicyLinesIntoModel] Not configured. Call configure() first.` | 500 | `loadPolicyLinesIntoModel` |
96
- | `[CasbinAuthorizationEnforcer] Cached payload is not an array of policy lines.` | - (logged, not thrown) | `parseCachedPolicyLines` - corrupt entry is discarded + refetched |
97
-
98
- > A corrupted Redis cache entry does **not** raise an error - it is logged and discarded, and the
99
- > lines are refetched from the adapter (the request never 500s on cache corruption).
100
-
101
- ### Registry Errors (AuthorizationEnforcerRegistry)
102
-
103
- | Error Message | Status | Method |
104
- |---------------|--------|--------|
105
- | `[AuthorizationEnforcerRegistry] Enforcer "{{name}}" does not support cache invalidation` | 500 | `invalidateUserCache` / `rebuildUserCache` (the resolved enforcer lacks the optional method) |
98
+ > There is no `Invalid cached.driver` runtime error - `cached` is a typed discriminated union (`{ use: false } | { use: true; driver: 'redis'; ... }`), so an unsupported cache driver is a compile-time error. Caching is **Redis-only**.
99
+
100
+ ### Casbin Enforcer Errors - Runtime (`buildRules` / `evaluate` / cache management)
101
+
102
+ | Error Message | Status | Method | Cause |
103
+ |---------------|--------|--------|-------|
104
+ | `[CasbinAuthorizationEnforcer] Not configured. Call configure() first.` | 400 | `evaluate` | `evaluate()` ran before `configure()` built the enforcer pool - should not happen via `resolveEnforcer()` |
105
+ | `[CasbinAuthorizationEnforcer] request.action and request.resource are required.` | 400 | `evaluate` | Malformed `IAuthorizationRequest` passed to `evaluate()` |
106
+ | `[CasbinAuthorizationEnforcer] keyFn returned an empty cache key.` | 400 | `resolveCacheKey` (via `buildRules`, `invalidateUserCache`, `rebuildUserCache`) | `cached.options.keyFn` returned a falsy value for this user |
107
+ | `[CasbinAuthorizationEnforcer] Cache management requires the redis cache driver, but caching is disabled.` | 400 | `invalidateUserCache` / `rebuildUserCache` (via `requireRedisCache`) | Called with `cached: { use: false }` |
108
+ | `[extractUserLines] Adapter does not support loadFilteredPolicy.` | 400 | `buildRules` (via `extractUserLines`) | `options.adapter` doesn't implement casbin's `FilteredAdapter.loadFilteredPolicy` |
109
+ | `[loadPolicyLinesIntoModel] Not configured. Call configure() first.` | 400 | `evaluate` (via `loadPolicyLinesIntoModel`) | Same root cause as the `evaluate` "Not configured" row, different call site |
110
+
111
+ **Not an error** - `[CasbinAuthorizationEnforcer] Cached payload is not an array of policy lines.` is logged at `warn` level by `parseCachedPolicyLines` and never thrown up to the caller: a corrupted or legacy Redis entry is discarded and the lines are refetched from the adapter, so the request never 500s (or 400s) on cache corruption.
106
112
 
107
113
  ## Troubleshooting
108
114
 
@@ -110,85 +116,73 @@ All error messages from the authorization module, organized by source:
110
116
 
111
117
  **Cause:** `AuthorizeComponent` was registered but `IAuthorizeOptions` was not bound to the container.
112
118
 
113
- **Fix:** Bind options **before** registering the component:
119
+ **Fix:** Bind options **before** registering the component.
114
120
 
115
121
  ```typescript
116
- // 1. Bind options first
117
122
  this.bind<IAuthorizeOptions>({ key: AuthorizeBindingKeys.OPTIONS }).toValue({
118
123
  defaultDecision: 'deny',
119
124
  alwaysAllowRoles: ['999_super-admin'],
120
125
  });
121
126
 
122
- // 2. Then register the component
123
127
  this.component(AuthorizeComponent);
124
128
  ```
125
129
 
126
130
  ### "Authorization failed: No authenticated user found"
127
131
 
128
- **Cause:** The authorization middleware runs after authentication, but no user was found on the context (`Authentication.CURRENT_USER` is undefined). This happens when:
129
- - The route has `authorize` but no `authenticate` config
130
- - Authentication middleware failed silently
131
- - Authentication was skipped but authorization was not
132
+ **Cause:** The authorization middleware runs after authentication, but no user was found on the context (`Authentication.CURRENT_USER` is `undefined`). Common triggers: the route has `authorize` but no `authenticate`, authentication was skipped but authorization wasn't, or the request genuinely has no valid credentials.
132
133
 
133
- **Fix:** Ensure routes with `authorize` also have `authenticate`:
134
+ **Fix:** Every route with `authorize` needs a matching `authenticate` config.
134
135
 
135
136
  ```typescript
136
137
  this.defineRoute({
137
138
  configs: {
138
139
  path: '/',
139
140
  method: 'get',
140
- authenticate: { strategies: [Authentication.STRATEGY_JWT] }, // Must be present
141
+ authenticate: { strategies: [Authentication.STRATEGY_JWT] }, // must be present
141
142
  authorize: { action: AuthorizationActions.READ, resource: 'Article' },
142
143
  // ...
143
144
  },
144
- handler: async (context) => { ... },
145
+ handler: async context => { /* ... */ },
145
146
  });
146
147
  ```
147
148
 
148
149
  ### "Authorization failed: user.principalType is required"
149
150
 
150
- **Cause:** The authenticated user object does not have a `principalType` field. This is required for enforcer-based authorization because the enforcer uses `principalType` to construct the casbin subject (e.g., `user_123`, `service_456`).
151
+ **Cause:** The authenticated user object has no `principalType` field. Enforcer-based authorization uses it to build the casbin subject (e.g. `User_123`).
151
152
 
152
- **Fix:** Ensure your authentication service sets `principalType` on the user object:
153
+ **Fix:** Set `principalType` when you build the user in your authentication service or token payload.
153
154
 
154
155
  ```typescript
155
- // In your JWT token service or authentication service:
156
156
  return {
157
157
  userId: '123',
158
- principalType: 'user', // Required for authorization
158
+ principalType: 'User', // required for authorization
159
159
  roles: [...],
160
160
  };
161
161
  ```
162
162
 
163
163
  > [!NOTE]
164
- > `principalType` is accessed via the `IAuthUser` index signature (`[extra: string | symbol]: any`), not a dedicated field. It must be set as a property on the returned user object.
164
+ > `principalType` is read via the `IAuthUser` index signature, not a dedicated field - it must be a real property on the returned user object.
165
165
 
166
166
  ### "Authorization denied by voter | action: ... | resource: ..."
167
167
 
168
- **Cause:** A voter function explicitly returned `AuthorizationDecisions.DENY` for the request.
168
+ **Cause:** A voter function explicitly returned `AuthorizationDecisions.DENY`.
169
169
 
170
- **Fix:** Check the voter logic. Review which voter denied the request by examining the action and resource in the error message. Common causes:
171
- - Voter checking ownership and user is not the owner
172
- - Voter checking time window and request is outside allowed hours
173
- - Voter checking resource state (e.g., locked, archived)
170
+ **Fix:** Inspect the voter named in your logs. Common causes: ownership check failed, a time-window check rejected the request, or a resource-state check (locked/archived) blocked it.
174
171
 
175
172
  ### "Authorization denied | action: ... | resource: ..."
176
173
 
177
- **Cause:** The enforcer's `evaluate()` returned `DENY` or `ABSTAIN` (which fell back to `defaultDecision`) for the requested action/resource. This means:
178
- - No matching policies were found for the user
179
- - Matching policies exist but deny the action
180
- - The casbin model does not cover the requested action/resource combination
174
+ **Cause:** `enforcer.evaluate()` returned `DENY`, or `ABSTAIN` and it fell back to `defaultDecision`. Usually: no matching policy for the user, a matching `deny` policy, or the model doesn't cover the requested domain/resource/action combination.
181
175
 
182
- **Fix:** Debug by checking:
176
+ **Fix:** Work through this checklist:
177
+
178
+ 1. **Policies are loaded correctly** - verify the adapter returns the right rows for this user (`ScopedCasbinAdapter` reads `PolicyDefinition` filtered by `subject_type`/`subject_id`).
179
+ 2. **Subject format matches.** `normalizePayloadFn` (or the default scoped payload) must produce a subject that matches what's stored, e.g. `User_123`.
180
+ 3. **The model covers the request.** For a custom (non-scoped) `.conf`, confirm the matcher handles the action/resource/domain shape you're sending.
181
+ 4. **Set `defaultDecision` explicitly** - don't rely on an implicit fallback:
183
182
 
184
- 1. **Policies are loaded correctly** -- verify your adapter is returning the right policy definitions for the user
185
- 2. **Subject format matches** -- the `normalizePayloadFn` must produce subjects matching your policy definitions (e.g., `user_123` must match what's in the database)
186
- 3. **Casbin model covers the request** -- your `.conf` file must define matchers for the action/resource/domain pattern you're using
187
- 4. **Set defaultDecision explicitly:**
188
183
  ```typescript
189
184
  this.bind<IAuthorizeOptions>({ key: AuthorizeBindingKeys.OPTIONS }).toValue({
190
- defaultDecision: 'deny', // Explicit is better
191
- // ...
185
+ defaultDecision: 'deny', // explicit is better than implicit
192
186
  });
193
187
  ```
194
188
 
@@ -196,108 +190,82 @@ this.bind<IAuthorizeOptions>({ key: AuthorizeBindingKeys.OPTIONS }).toValue({
196
190
 
197
191
  **Cause:** Two or more enforcers in the same `register()` call have the same name.
198
192
 
199
- **Fix:** Ensure each enforcer has a unique name:
193
+ **Fix:** Give every enforcer in the call a unique name.
200
194
 
201
195
  ```typescript
202
196
  AuthorizationEnforcerRegistry.getInstance().register({
203
197
  container: this,
204
198
  enforcers: [
205
- { enforcer: CasbinEnforcer, name: 'casbin', type: 'casbin', ... },
206
- { enforcer: CustomEnforcer, name: 'custom', type: 'custom', ... }, // Different name
199
+ { enforcer: CasbinAuthorizationEnforcer, name: 'casbin', type: 'casbin', options: { /* ... */ } },
200
+ { enforcer: MyCustomEnforcer, name: 'custom', type: 'custom' }, // different name
207
201
  ],
208
202
  });
209
203
  ```
210
204
 
211
205
  ### "[AuthorizationEnforcerRegistry] Enforcer already registered: ..."
212
206
 
213
- **Cause:** An enforcer with this name was already registered in a previous `register()` call. The registry does not allow overwriting.
214
-
215
- **Fix:** Ensure you only register each enforcer name once. If you need to re-register, call `registry.reset()` first (typically only in tests).
216
-
217
- ### "[AuthorizationEnforcerRegistry] No items registered"
207
+ **Cause:** An enforcer with this name was already registered in a previous `register()` call - the registry doesn't allow overwriting.
218
208
 
219
- **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.
220
-
221
- **Fix:** Register enforcers after registering the component:
222
-
223
- ```typescript
224
- // 1. Bind options and register component
225
- this.bind<IAuthorizeOptions>({ key: AuthorizeBindingKeys.OPTIONS }).toValue({ ... });
226
- this.component(AuthorizeComponent);
227
-
228
- // 2. Register enforcers
229
- AuthorizationEnforcerRegistry.getInstance().register({
230
- container: this,
231
- enforcers: [{ enforcer: CasbinAuthorizationEnforcer, name: 'casbin', type: 'casbin', options: { ... } }],
232
- });
233
- ```
209
+ **Fix:** Register each name once. Call `AuthorizationEnforcerRegistry.getInstance().reset()` first if you genuinely need to re-register (typically only in tests).
234
210
 
235
211
  ### "[AuthorizationEnforcerRegistry] Descriptor not found: ..."
236
212
 
237
- **Cause:** Tried to resolve an enforcer by a name that was never registered. This happens when `enforcerName` is specified in the `authorize()` call but doesn't match any registered enforcer.
213
+ **Cause:** `enforcerName` in an `authorize()` call doesn't match any registered enforcer name.
238
214
 
239
- **Fix:** Ensure the enforcer name matches what was registered. The default enforcer name is the first one registered.
215
+ **Fix:** Match the name used in `register()`. The default (when `enforcerName` is omitted) is the first one registered.
240
216
 
241
217
  ### "[AuthorizationEnforcerRegistry] Failed to resolve: ..."
242
218
 
243
- **Cause:** The enforcer class was registered in the descriptor Map but DI container resolution returned `null`. This typically means the enforcer class has unsatisfied dependencies.
219
+ **Cause:** The enforcer class is registered, but DI resolution returned `null` - typically an unsatisfied `@inject` dependency in its constructor.
244
220
 
245
- **Fix:** Check that all `@inject` dependencies in your enforcer's constructor are bound in the same container.
221
+ **Fix:** Confirm every `@inject` in the enforcer's constructor is bound in the same container before `resolveEnforcer()` runs.
246
222
 
247
- ### "[CasbinAuthorizationEnforcer] casbin is not installed"
223
+ ### "[CasbinAuthorizationEnforcer] "casbin" is not installed"
248
224
 
249
- **Cause:** The Casbin enforcer dynamically imports `casbin` at configure time, but the package is not installed.
225
+ **Cause:** The Casbin enforcer dynamically imports `casbin` in `configure()`, but the package isn't installed.
250
226
 
251
- **Fix:** Install casbin as a dependency:
227
+ **Fix:**
252
228
 
253
229
  ```bash
254
230
  bun add casbin
255
231
  ```
256
232
 
257
- ### "[CasbinAuthorizationEnforcer] options.model is required"
233
+ ### "[CasbinAuthorizationEnforcer] options.model is required."
258
234
 
259
- **Cause:** The Casbin enforcer's options do not include a `model` field. The model defines the casbin RBAC/ABAC rules structure.
235
+ **Cause:** The enforcer options have no `model`.
260
236
 
261
- **Fix:** Provide `model` in the enforcer options:
237
+ **Fix:** Provide a model - inline text (recommended: `CASBIN_RBAC_DOMAIN_SCOPED_MODEL`) or a file path.
262
238
 
263
239
  ```typescript
264
- AuthorizationEnforcerRegistry.getInstance().register({
265
- container: this,
266
- enforcers: [{
267
- enforcer: CasbinAuthorizationEnforcer,
268
- name: 'casbin',
269
- type: 'casbin',
270
- options: {
271
- model: {
272
- driver: 'file',
273
- definition: path.resolve(__dirname, './security/model.conf'),
274
- },
275
- cached: { use: false },
276
- },
277
- }],
278
- });
240
+ options: {
241
+ model: { driver: CasbinEnforcerModelDrivers.TEXT, definition: CASBIN_RBAC_DOMAIN_SCOPED_MODEL },
242
+ isScoped: true,
243
+ adapter,
244
+ cached: { use: false },
245
+ }
279
246
  ```
280
247
 
281
- ### "[extractUserLines] Adapter does not support loadFilteredPolicy"
248
+ ### "[registerMatchers] Role definition "g2" is not declared in the Casbin model..."
282
249
 
283
- **Cause:** The adapter provided to the Casbin enforcer does not implement the `loadFilteredPolicy` method from casbin's `FilteredAdapter` interface. The authorization system always uses filtered policy loading.
250
+ **Cause:** `domainMatching.roleDefinition` references a relation the model doesn't declare under `[role_definition]`. Casbin would otherwise register the function as a silent no-op, leaving wildcard domains permanently unmatched (global roles silently denied) - the enforcer throws at boot instead.
284
251
 
285
- **Fix:** Use an adapter that implements `FilteredAdapter`, such as `ScopedCasbinAdapter` or a custom adapter extending `BaseFilteredAdapter`:
252
+ **Fix:** Point `roleDefinition` at a relation the model actually declares.
286
253
 
287
254
  ```typescript
288
- import { ScopedCasbinAdapter } from '@venizia/ignis';
289
-
290
- const adapter = new ScopedCasbinAdapter({
291
- dataSource,
292
- entities: { /* IScopedCasbinEntities */ },
293
- });
255
+ domainMatching: { roleDefinition: 'g', fn: CasbinDomainMatchingFunctions.KEY_MATCH } // model must declare `g = _, _, _`
294
256
  ```
295
257
 
258
+ ### "[CasbinAuthorizationEnforcer] Matcher smoke test failed at warmup..."
259
+
260
+ **Cause:** `assertMatcherCompilesSync()` runs a dummy `enforceSync()` at warmup to force casbin's lazy matcher compile. It failed - a matcher syntax error, a function referenced in the matcher that was never registered, or a request-arity mismatch (3 vs. 4 tokens).
261
+
262
+ **Fix:** Check the `[matchers]` section of your `.conf`. If scoped, confirm `isScoped: true` is set (registers `objectMatch` + `keyMatch` automatically); if using a custom flat model, confirm every function the matcher calls is registered via `domainMatching`.
263
+
296
264
  ### "[CasbinAuthorizationEnforcer] cached.options.expiresIn must be >= 10000"
297
265
 
298
- **Cause:** The `expiresIn` value for the cache TTL is too small. Minimum is 10,000 ms (10 seconds), enforced by the `MIN_EXPIRES_IN` constant.
266
+ **Cause:** `expiresIn` is below the 10,000 ms minimum (`MIN_EXPIRES_IN`).
299
267
 
300
- **Fix:** Increase the expiration time:
268
+ **Fix:**
301
269
 
302
270
  ```typescript
303
271
  cached: {
@@ -305,53 +273,42 @@ cached: {
305
273
  driver: 'redis',
306
274
  options: {
307
275
  connection: redisHelper,
308
- expiresIn: 5 * 60 * 1000, // 5 minutes (minimum: 10,000 ms)
309
- keyFn: ({ user }) => `authz:policies:${user.userId}`,
276
+ expiresIn: 5 * 60 * 1000, // 5 minutes (minimum: 10,000 ms)
277
+ keyFn: ({ user }) => `authz:policies:${user.principalType}:${user.userId}`,
310
278
  },
311
279
  },
312
280
  ```
313
281
 
314
282
  ### "[CasbinAuthorizationEnforcer] Not configured. Call configure() first."
315
283
 
316
- **Cause:** The Casbin enforcer's `evaluate()` (or `loadPolicyLinesIntoModel()`) was called before `configure()` built the enforcer pool. This should not happen when using `resolveEnforcer()` (which auto-configures), but can occur if the enforcer is resolved manually via the DI container.
284
+ **Cause:** `evaluate()` (or its internal `loadPolicyLinesIntoModel()`) ran before `configure()` built the enforcer pool. This shouldn't happen through `resolveEnforcer()` (which auto-configures), but can occur if the enforcer is resolved manually from the DI container.
317
285
 
318
- **Fix:** Always resolve enforcers through the registry's `resolveEnforcer()` method, which handles configure-once automatically:
286
+ **Fix:** Always go through the registry, which handles configure-once automatically.
319
287
 
320
288
  ```typescript
321
289
  const enforcer = await AuthorizationEnforcerRegistry.getInstance().resolveEnforcer({ name: 'casbin' });
322
- // configure() is called automatically on first resolve
323
290
  ```
324
291
 
325
- ### "[loadPoliciesWithRedisCache] Invalid cachedKey"
292
+ ### "[CasbinAuthorizationEnforcer] keyFn returned an empty cache key."
326
293
 
327
- **Cause:** The `keyFn` in Redis cache options returned a falsy value (empty string, null, undefined). The cache key is required to store/retrieve policies from Redis.
294
+ **Cause:** `cached.options.keyFn` returned a falsy value for a user.
328
295
 
329
- **Fix:** Ensure your `keyFn` always returns a non-empty string:
296
+ **Fix:** Always return a stable, non-empty key.
330
297
 
331
298
  ```typescript
332
- keyFn: ({ user }) => {
333
- if (!user.userId) {
334
- throw new Error('User ID is required for cache key');
335
- }
336
- return `authz:policies:${user.userId}`;
337
- },
299
+ keyFn: ({ user }) => `authz:policies:${user.principalType}:${user.userId}`,
338
300
  ```
339
301
 
340
- ### "[CasbinAuthorizationEnforcer] Not configured. Call configure() first."
341
-
342
- **Cause:** `evaluate()` or `loadPolicyLinesIntoModel()` ran before the enforcer pool was built.
343
-
344
- **Fix:** Ensure `configure()` completed successfully (the registry calls it on first use) before any
345
- evaluation. The enforcer pool is created in `configure()`.
346
-
347
- ### "[CasbinAuthorizationEnforcer] keyFn returned an empty cache key."
302
+ ### "[extractUserLines] Adapter does not support loadFilteredPolicy"
348
303
 
349
- **Cause:** The Redis `cached.options.keyFn` returned an empty string for a user.
304
+ **Cause:** `options.adapter` doesn't implement casbin's `FilteredAdapter.loadFilteredPolicy`. Authorization always loads policies filtered by principal.
350
305
 
351
- **Fix:** Return a stable, non-empty key (commonly derived from `user.principalType` + `user.userId`):
306
+ **Fix:** Use `ScopedCasbinAdapter`, or extend `BaseFilteredAdapter` and implement `loadFilteredPolicy`.
352
307
 
353
308
  ```typescript
354
- keyFn: ({ user }) => `authz:policies:${user.principalType}:${user.userId}`,
309
+ import { ScopedCasbinAdapter } from '@venizia/ignis';
310
+
311
+ const adapter = new ScopedCasbinAdapter({ dataSource, entities: { /* IScopedCasbinEntities */ } });
355
312
  ```
356
313
 
357
314
  ### "[resolveModel] Invalid model.driver | Valids: [file, text]"
@@ -360,39 +317,44 @@ keyFn: ({ user }) => `authz:policies:${user.principalType}:${user.userId}`,
360
317
 
361
318
  **Fix:** Use `CasbinEnforcerModelDrivers.FILE` (`'file'`) or `CasbinEnforcerModelDrivers.TEXT` (`'text'`).
362
319
 
363
- > There is no longer an `Invalid cached.driver` runtime error - `cached` is a typed union, so an
364
- > unsupported cache driver is caught at compile time. Caching is **Redis-only**.
320
+ ### "[CasbinAuthorizationEnforcer] Cache management requires the redis cache driver, but caching is disabled."
321
+
322
+ **Cause:** `invalidateUserCache()` or `rebuildUserCache()` was called on an enforcer configured with `cached: { use: false }`.
323
+
324
+ **Fix:** Either configure Redis caching, or don't call the cache-management methods for a non-cached enforcer.
365
325
 
366
326
  ## Common Patterns
367
327
 
368
- ### Authorization Is Not Running
328
+ ### Authorization is not running
329
+
330
+ Check, in order:
331
+
332
+ 1. `authorize` is actually set on the route config (not just `authenticate`).
333
+ 2. `authenticate` isn't `{ skip: true }` - that skips authorization too, in both raw route configs and the CRUD factory.
334
+ 3. The component is registered AND at least one enforcer is registered via `AuthorizationEnforcerRegistry`.
335
+ 4. If no enforcers are registered, the middleware skips authorization silently (`next()`) rather than throwing - this is often the real cause of "authorization does nothing" during rollout.
369
336
 
370
- Check middleware injection order:
371
- 1. Verify `authorize` is set on the route config (not just `authenticate`)
372
- 2. Verify `authenticate` is not set to `{ skip: true }` (which also skips authorization in CRUD factory)
373
- 3. Verify the component is registered and enforcers are registered via the registry
374
- 4. Note: if no enforcers are registered, the middleware skips authorization silently (calls `next()`) rather than throwing an error
337
+ ### Rules are rebuilt on every request
375
338
 
376
- ### Rules Are Built On Every Request
339
+ Rules cache on `Authorization.RULES`, but only **within one request**:
377
340
 
378
- Check that rules are being cached correctly. The middleware caches on `Authorization.RULES`:
379
- - Cached per-request (each new request starts fresh)
380
- - Multiple authorization specs on the same route share the cache
381
- - If rules are `undefined` or `null`, they will be rebuilt
341
+ - A new HTTP request always starts with an empty cache.
342
+ - Multiple `authorize` specs on the same route share the cache - only the first builds.
343
+ - `undefined`/`null` on `Authorization.RULES` triggers a rebuild - `c.set(Authorization.RULES, null)` forces one mid-request.
382
344
 
383
- ### Casbin Policies Not Loading
345
+ ### Casbin policies not loading
384
346
 
385
- 1. **Check adapter entities** -- ensure `IScopedCasbinEntities` (`policyDefinition`/`permission` table names + `schemaName`, `principals`, `domainTypes`) match your database schema
386
- 2. **Check the variant column** -- `PolicyDefinition.variant` must use the `AuthorizationPolicyVariants.*.action` values: `grant`, `assign_role`, `join_domain`, `role_inherits`, `resource_inherits`, `action_inherits`, `domain_inherits`
387
- 3. **Check subject/target types** -- the SQL queries filter by `subject_type`/`target_type` against `principals` and `domainTypes`
388
- 4. **Check the model** -- for scoped RBAC, use `CASBIN_RBAC_DOMAIN_SCOPED_MODEL` with `isScoped: true`
347
+ 1. **Adapter entities** - `IScopedCasbinEntities` (`policyDefinition`/`permission` table + schema names, `principals`, `domainTypes`) must match your actual database schema.
348
+ 2. **Variant column** - `PolicyDefinition.variant` must use `AuthorizationPolicyVariants.*.action` values: `grant`, `assign_role`, `join_domain`, `role_inherits`, `resource_inherits`, `action_inherits`, `domain_inherits`.
349
+ 3. **Subject/target types** - the adapter's SQL filters by `subject_type`/`target_type` against `principals` and `domainTypes`; a mismatch silently returns zero rows.
350
+ 4. **Model** - scoped RBAC needs `CASBIN_RBAC_DOMAIN_SCOPED_MODEL` with `isScoped: true` together; one without the other misfires.
389
351
 
390
- ### Redis Cache Not Working
352
+ ### Redis cache not working
391
353
 
392
- 1. **Check Redis connection** -- verify the `AbstractRedisHelper` subclass (`RedisSingleHelper`, `RedisClusterHelper`, or `RedisSentinelHelper`) is properly connected
393
- 2. **Check keyFn** -- ensure it returns a unique, non-empty key per user
394
- 3. **Check expiresIn** -- must be >= 10,000 ms (`MIN_EXPIRES_IN`)
395
- 4. **Verify cache hit** -- check logs for `"Loaded CACHED Policies"` vs `"Loaded ADAPTER + CACHED Policies"`
354
+ 1. **Connection** - verify the `IRedisHelper` (`RedisSingleHelper`/`RedisClusterHelper`/`RedisSentinelHelper`) is actually connected.
355
+ 2. **`keyFn`** - must return a unique, non-empty key per user.
356
+ 3. **`expiresIn`** - must be `>= 10_000` (`MIN_EXPIRES_IN`).
357
+ 4. **Corruption is silent** - a malformed cached payload is discarded and refetched (see the Casbin runtime error table above), so a "cache never seems to hit" symptom is more likely a `keyFn` mismatch than corruption.
396
358
 
397
359
  ## See Also
398
360