@venizia/ignis-docs 0.2.1-0 → 0.2.1-1

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 (145) hide show
  1. package/content/best-practices/architectural-patterns.md +3 -3
  2. package/content/best-practices/code-style-standards/naming-conventions.md +1 -1
  3. package/content/best-practices/contribution-workflow.md +2 -2
  4. package/content/best-practices/error-handling.md +94 -89
  5. package/content/best-practices/security-guidelines.md +5 -5
  6. package/content/extensions/components/api-reference.md +22 -21
  7. package/content/extensions/components/authentication/api.md +64 -28
  8. package/content/extensions/components/authentication/errors.md +19 -5
  9. package/content/extensions/components/authentication/index.md +19 -18
  10. package/content/extensions/components/authentication/usage.md +55 -30
  11. package/content/extensions/components/authorization/api.md +351 -84
  12. package/content/extensions/components/authorization/errors.md +51 -17
  13. package/content/extensions/components/authorization/getting-started.md +227 -0
  14. package/content/extensions/components/authorization/index.md +24 -16
  15. package/content/extensions/components/authorization/usage.md +45 -21
  16. package/content/extensions/components/health-check.md +15 -9
  17. package/content/extensions/components/index.md +24 -90
  18. package/content/extensions/components/mail/api.md +105 -54
  19. package/content/extensions/components/mail/errors.md +12 -10
  20. package/content/extensions/components/mail/index.md +32 -13
  21. package/content/extensions/components/mail/usage.md +26 -18
  22. package/content/extensions/components/request-tracker.md +18 -14
  23. package/content/extensions/components/socket-io/api.md +377 -882
  24. package/content/extensions/components/socket-io/errors.md +49 -51
  25. package/content/extensions/components/socket-io/index.md +72 -88
  26. package/content/extensions/components/socket-io/usage.md +107 -117
  27. package/content/extensions/components/static-asset/api.md +83 -31
  28. package/content/extensions/components/static-asset/errors.md +18 -7
  29. package/content/extensions/components/static-asset/index.md +18 -11
  30. package/content/extensions/components/static-asset/usage.md +11 -6
  31. package/content/extensions/components/template/index.md +3 -3
  32. package/content/extensions/components/websocket/api.md +58 -27
  33. package/content/extensions/components/websocket/errors.md +3 -3
  34. package/content/extensions/components/websocket/index.md +8 -7
  35. package/content/extensions/components/websocket/usage.md +21 -8
  36. package/content/extensions/helpers/cron/index.md +8 -7
  37. package/content/extensions/helpers/crypto/index.md +16 -8
  38. package/content/extensions/helpers/crypto/reference.md +96 -24
  39. package/content/extensions/helpers/env/index.md +14 -10
  40. package/content/extensions/helpers/error/index.md +99 -23
  41. package/content/extensions/helpers/index.md +61 -47
  42. package/content/extensions/helpers/inversion/index.md +23 -6
  43. package/content/extensions/helpers/inversion/reference.md +30 -22
  44. package/content/extensions/helpers/kafka/admin.md +3 -2
  45. package/content/extensions/helpers/kafka/compile-binary.md +69 -44
  46. package/content/extensions/helpers/kafka/consumer.md +24 -21
  47. package/content/extensions/helpers/kafka/examples.md +22 -234
  48. package/content/extensions/helpers/kafka/index.md +30 -62
  49. package/content/extensions/helpers/kafka/producer.md +31 -26
  50. package/content/extensions/helpers/kafka/schema-registry.md +19 -14
  51. package/content/extensions/helpers/logger/hf-logger.md +49 -22
  52. package/content/extensions/helpers/logger/index.md +37 -12
  53. package/content/extensions/helpers/logger/pino.md +31 -11
  54. package/content/extensions/helpers/logger/reference.md +243 -52
  55. package/content/extensions/helpers/network/api.md +65 -30
  56. package/content/extensions/helpers/network/index.md +32 -11
  57. package/content/extensions/helpers/queue/index.md +17 -6
  58. package/content/extensions/helpers/queue/reference.md +52 -25
  59. package/content/extensions/helpers/redis/index.md +29 -11
  60. package/content/extensions/helpers/redis/reference.md +85 -28
  61. package/content/extensions/helpers/secrets/index.md +82 -12
  62. package/content/extensions/helpers/socket-io/api.md +40 -21
  63. package/content/extensions/helpers/socket-io/index.md +26 -10
  64. package/content/extensions/helpers/storage/api.md +42 -24
  65. package/content/extensions/helpers/storage/index.md +10 -7
  66. package/content/extensions/helpers/types/index.md +20 -7
  67. package/content/extensions/helpers/types/reference.md +53 -14
  68. package/content/extensions/helpers/uid/index.md +166 -10
  69. package/content/extensions/helpers/websocket/api.md +52 -13
  70. package/content/extensions/helpers/websocket/index.md +7 -4
  71. package/content/extensions/helpers/worker-thread/index.md +9 -5
  72. package/content/extensions/helpers/worker-thread/reference.md +20 -20
  73. package/content/extensions/index.md +38 -39
  74. package/content/extensions/src-details/mcp-server.md +96 -548
  75. package/content/guides/core-concepts/persistent/datasources.md +2 -2
  76. package/content/guides/core-concepts/persistent/index.md +5 -1
  77. package/content/guides/core-concepts/persistent/pglite.md +218 -0
  78. package/content/guides/core-concepts/persistent/postgres-drivers.md +21 -15
  79. package/content/guides/core-concepts/persistent/search-meilisearch.md +22 -12
  80. package/content/guides/core-concepts/persistent/search-typesense.md +26 -14
  81. package/content/guides/core-concepts/persistent/sqlite.md +296 -0
  82. package/content/guides/core-concepts/persistent/transactions.md +12 -11
  83. package/content/guides/get-started/5-minute-quickstart.md +59 -206
  84. package/content/guides/get-started/philosophy.md +135 -670
  85. package/content/guides/get-started/setup.md +53 -74
  86. package/content/guides/migrations/redis-helpers-migration.md +4 -3
  87. package/content/guides/migrations/scoped-rbac-migration.md +5 -5
  88. package/content/guides/migrations/unified-connectors-migration.md +7 -6
  89. package/content/guides/tutorials/realtime-chat.md +1 -1
  90. package/content/references/base/application.md +63 -21
  91. package/content/references/base/components.md +3 -3
  92. package/content/references/base/connectors.md +14 -14
  93. package/content/references/base/controllers.md +12 -12
  94. package/content/references/base/datasources-reference.md +32 -31
  95. package/content/references/base/datasources.md +6 -6
  96. package/content/references/base/dependency-injection.md +12 -11
  97. package/content/references/base/filter-system/application-usage.md +54 -30
  98. package/content/references/base/filter-system/array-operators.md +24 -46
  99. package/content/references/base/filter-system/comparison-operators.md +47 -67
  100. package/content/references/base/filter-system/default-filter.md +59 -53
  101. package/content/references/base/filter-system/fields-order-pagination.md +92 -146
  102. package/content/references/base/filter-system/index.md +25 -13
  103. package/content/references/base/filter-system/json-filtering.md +45 -184
  104. package/content/references/base/filter-system/list-operators.md +23 -53
  105. package/content/references/base/filter-system/logical-operators.md +63 -121
  106. package/content/references/base/filter-system/null-operators.md +34 -104
  107. package/content/references/base/filter-system/pattern-matching.md +40 -55
  108. package/content/references/base/filter-system/quick-reference.md +86 -198
  109. package/content/references/base/filter-system/range-operators.md +18 -46
  110. package/content/references/base/filter-system/tips.md +6 -6
  111. package/content/references/base/filter-system/use-cases.md +33 -15
  112. package/content/references/base/grpc-controllers.md +53 -17
  113. package/content/references/base/index.md +5 -3
  114. package/content/references/base/middlewares.md +11 -10
  115. package/content/references/base/models-reference.md +17 -17
  116. package/content/references/base/models.md +4 -3
  117. package/content/references/base/providers.md +8 -8
  118. package/content/references/base/repositories/advanced.md +224 -326
  119. package/content/references/base/repositories/index.md +19 -6
  120. package/content/references/base/repositories/mixins.md +5 -5
  121. package/content/references/base/repositories/relations.md +160 -293
  122. package/content/references/base/repositories/soft-deletable.md +16 -6
  123. package/content/references/base/secrets.md +17 -13
  124. package/content/references/base/services.md +6 -4
  125. package/content/references/configuration/environment-variables.md +31 -23
  126. package/content/references/configuration/index.md +6 -4
  127. package/content/references/index.md +1 -1
  128. package/content/references/utilities/duration.md +85 -0
  129. package/content/references/utilities/index.md +5 -1
  130. package/content/references/utilities/jsx-reference.md +11 -11
  131. package/content/references/utilities/jsx.md +2 -2
  132. package/content/references/utilities/module.md +78 -25
  133. package/content/references/utilities/request.md +2 -1
  134. package/content/references/utilities/retry.md +139 -0
  135. package/content/references/utilities/schema.md +2 -2
  136. package/content/references/utilities/statuses-reference.md +4 -4
  137. package/content/references/utilities/statuses.md +5 -5
  138. package/dist/mcp-server/index.js +0 -0
  139. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts +1 -1
  140. package/dist/mcp-server/tools/github/list-project-files.tool.d.ts.map +1 -1
  141. package/dist/mcp-server/tools/github/list-project-files.tool.js +3 -3
  142. package/dist/mcp-server/tools/github/search-code.tool.js +1 -1
  143. package/dist/mcp-server/tools/github/verify-dependencies.tool.js +1 -1
  144. package/dist/mcp-server/tools/github/view-source-file.tool.js +1 -1
  145. package/package.json +17 -16
@@ -10,10 +10,30 @@ Every option, binding key, class, and method the Authorization component exposes
10
10
 
11
11
  **Files:**
12
12
 
13
- - [`packages/core/src/components/auth/authorize/`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize) - component, providers, enforcers, adapters, models, middleware
14
- - [`packages/core/src/components/auth/base/abstract-auth-registry.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/base/abstract-auth-registry.ts) - `AbstractAuthRegistry` (shared with Authentication)
15
- - [`packages/core/src/base/metadata/persistents.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/metadata/persistents.ts) - `@model` populating `AUTHORIZATION_SUBJECT`
16
- - [`packages/core/src/helpers/inversion/mixins/model.mixin.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/helpers/inversion/mixins/model.mixin.ts) - `MetadataRegistry` authorize-settings queries
13
+ - [`packages/core-server/src/components/auth/authorize/`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize) - component, providers, enforcers, adapters, models, middleware
14
+ - [`packages/core-server/src/components/auth/base/abstract-auth-registry.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/base/abstract-auth-registry.ts) - `AbstractAuthRegistry` (shared with Authentication)
15
+ - [`packages/core-server/src/base/metadata/persistents.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/base/metadata/persistents.ts) - `@model` populating `AUTHORIZATION_SUBJECT`
16
+ - [`packages/core-server/src/helpers/inversion/mixins/model.mixin.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/helpers/inversion/mixins/model.mixin.ts) - `MetadataRegistry` authorize-settings queries
17
+
18
+ ## Find what you need
19
+
20
+ | You want to | Go to |
21
+ |---|---|
22
+ | Bind global options and per-enforcer options | [Binding keys](#binding-keys) |
23
+ | Configure `AuthorizeComponent` at startup | [IAuthorizeOptions](#iauthorizeoptions) |
24
+ | Configure the Casbin enforcer (model, cache, pool) | [ICasbinEnforcerOptions](#icasbinenforceroptions) |
25
+ | Write a route's `authorize` spec | [IAuthorizationSpec (route-level)](#iauthorizationspec-route-level) |
26
+ | Resolve a request's domain scope | [IAuthorizationDomainSource / TAuthorizationDomainResolver](#iauthorizationdomainsource-tauthorizationdomainresolver) |
27
+ | Look up an action, decision, or role constant | [Constants](#constants) |
28
+ | Read the scoped RBAC `.conf` model | [CASBIN_RBAC_DOMAIN_SCOPED_MODEL](#casbin_rbac_domain_scoped_model) |
29
+ | Register or resolve enforcers | [AuthorizationEnforcerRegistry](#authorizationenforcerregistry) |
30
+ | Understand how the Casbin enforcer builds and evaluates rules | [CasbinAuthorizationEnforcer](#casbinauthorizationenforcer) |
31
+ | Use the ready-made Postgres adapter | [ScopedCasbinAdapter](#scopedcasbinadapter) |
32
+ | Write a custom adapter | [BaseFilteredAdapter](#basefilteredadapter) |
33
+ | Grant a subset of a subject's operations | [Subset grants (custom rows)](#subset-grants-custom-rows) and [GrantBuilder.planGrant](#grantbuilderplangrant) |
34
+ | Seed `PolicyDefinition` / `Permission` rows | [Policy and permission builders](#policy-and-permission-builders) |
35
+ | Wire `authorize` into a REST or gRPC controller | [Controller integration](#controller-integration) |
36
+ | Read the context keys the middleware sets | [Context variables](#context-variables) |
17
37
 
18
38
  ## Import paths
19
39
 
@@ -29,7 +49,7 @@ import {
29
49
  CasbinAuthorizationEnforcer,
30
50
 
31
51
  // Adapters
32
- BaseFilteredAdapter, ScopedCasbinAdapter,
52
+ BaseFilteredAdapter, ScopedCasbinAdapter, PrincipalPolicyEdges,
33
53
 
34
54
  // Scoped RBAC model
35
55
  CASBIN_RBAC_DOMAIN_SCOPED_MODEL,
@@ -37,11 +57,9 @@ import {
37
57
  // Models
38
58
  AuthorizationRole,
39
59
 
40
- // Policy / permission catalog builders
41
- AuthorizationPolicyBuilder, AuthorizationPermissionBuilder,
42
-
43
- // Resource-hierarchy matcher (register on a custom Casbin model)
44
- objectMatch,
60
+ // Policy / permission catalog builders - AuthorizationPermissionBuilder.objectMatch is the
61
+ // resource-hierarchy matcher (register on a custom Casbin model)
62
+ AuthorizationPolicyBuilder, AuthorizationPermissionBuilder, GrantBuilder,
45
63
 
46
64
  // Constants
47
65
  Authorization, AuthorizationActions, AuthorizationDecisions, AuthorizationDomainScopes,
@@ -115,7 +133,8 @@ flowchart TD
115
133
  Voters -->|DENY| E403a[/403 Denied by voter/]
116
134
  Voters -->|ALLOW| Next4([next - voter allow])
117
135
  Voters -->|ABSTAIN / none| HasEnforcers{Enforcers registered?}
118
- HasEnforcers -->|No| Next6([next - skip, no enforcers])
136
+ HasEnforcers -->|No, defaultDecision: allow| Next6([next - allow, warning logged])
137
+ HasEnforcers -->|No, defaultDecision: deny or unset| E403c[/403 no enforcer registered/]
119
138
  HasEnforcers -->|Yes| Resolve[Resolve enforcer by name]
120
139
  Resolve --> ResolveDomain["Resolve domain (if spec.domain or domainResolver)"]
121
140
  ResolveDomain --> Cache{Rules cached?}
@@ -201,13 +220,12 @@ components/auth/authorize/
201
220
  │ ├── base-filtered.ts # BaseFilteredAdapter (thin abstract) + ICasbinPolicyFilter
202
221
  │ ├── scoped-casbin.adapter.ts # ScopedCasbinAdapter (generic edge-table reader)
203
222
  │ └── types.ts # IScopedCasbinEntities, IScopedCasbinTable, ICasbinPolicySource
223
+ ├── builders/
224
+ │ ├── grant.builder.ts # GrantBuilder
225
+ │ ├── permission.builder.ts # AuthorizationPermissionBuilder + static objectMatch
226
+ │ └── policy.builder.ts # AuthorizationPolicyBuilder
204
227
  ├── common/
205
- │ ├── constants.ts # Authorization, Actions, Decisions, PolicyVariants, Roles, ...
206
- │ ├── keys.ts # AuthorizeBindingKeys
207
- │ ├── object-match.ts # objectMatch resource-hierarchy matcher
208
- │ ├── permission-builder.ts # AuthorizationPermissionBuilder
209
- │ ├── policy-builder.ts # AuthorizationPolicyBuilder
210
- │ ├── resolve-request-domain.ts # resolveRequestDomain
228
+ │ ├── constants.ts # Authorization, Actions, Decisions, PolicyVariants, Roles, AuthorizeBindingKeys, ...
211
229
  │ └── types.ts # IAuthorizeOptions, IAuthorizationEnforcer, ICasbinEnforcerOptions, ...
212
230
  ├── enforcers/
213
231
  │ ├── casbin.enforcer.ts # CasbinAuthorizationEnforcer
@@ -215,7 +233,9 @@ components/auth/authorize/
215
233
  │ └── models/rbac-domain.model.ts # CASBIN_RBAC_DOMAIN_SCOPED_MODEL
216
234
  ├── middlewares/authorize.middleware.ts # authorize() standalone function
217
235
  ├── models/authorization-role.model.ts # AuthorizationRole
218
- ├── providers/authorization.provider.ts # AuthorizationProvider
236
+ ├── providers/
237
+ │ ├── authorization.provider.ts # AuthorizationProvider
238
+ │ └── request-domain.ts # resolveRequestDomain, readDeclarative
219
239
  └── component.ts # AuthorizeComponent
220
240
  ```
221
241
 
@@ -230,7 +250,7 @@ components/auth/authorize/
230
250
  | Rules caching | Built rules cached on the Hono context per-request - avoids rebuilding for multi-spec routes |
231
251
  | Registry singleton | Mirrors `AuthenticationStrategyRegistry` - shares `AbstractAuthRegistry<T>` |
232
252
  | Filtered adapter pattern | `BaseFilteredAdapter` is a thin read-only base; subclasses implement only `loadFilteredPolicy` |
233
- | No-enforcer fallback | No enforcers registered -> the middleware skips authorization and calls `next()` instead of throwing |
253
+ | No-enforcer fallback | No enforcers registered -> the middleware honors `defaultDecision`: `deny` (default) throws a named 403, `allow` proceeds and logs a warning |
234
254
  | Single edge table (scoped model) | `ScopedCasbinAdapter` reads one `PolicyDefinition` table for every edge type - no per-relation tables |
235
255
 
236
256
  ## AuthorizeComponent
@@ -253,7 +273,7 @@ class AuthorizeComponent extends BaseComponent {
253
273
  > [!NOTE]
254
274
  > Enforcer registration is separate - `AuthorizeComponent` only validates global options. Register enforcers via `AuthorizationEnforcerRegistry.register()`.
255
275
 
256
- Source -> [`component.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/component.ts)
276
+ Source -> [`component.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/component.ts)
257
277
 
258
278
  ## Binding keys
259
279
 
@@ -275,7 +295,7 @@ class AuthorizeBindingKeys {
275
295
 
276
296
  `AuthorizeBindingKeys.enforcerOptions(name)` is called automatically by `AuthorizationEnforcerRegistry.register()` when `options` is provided; `CasbinAuthorizationEnforcer` injects its options from `AuthorizeBindingKeys.enforcerOptions('casbin')`.
277
297
 
278
- Source -> [`common/keys.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/common/keys.ts)
298
+ Source -> [`common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/common/constants.ts)
279
299
 
280
300
  ## Option interfaces
281
301
 
@@ -306,7 +326,7 @@ Casbin-specific options, provided per-enforcer via `AuthorizationEnforcerRegistr
306
326
  | `model` | `{ driver: 'file', definition } \| { driver: 'text', definition }` | - | **Required.** Casbin model (file path or inline text). For scoped RBAC, use `CASBIN_RBAC_DOMAIN_SCOPED_MODEL` |
307
327
  | `cached` | `{ use: false } \| (ICasbinEnforcerCachedRedis & { use: true })` | - | **Required.** Caching configuration (Redis-only) |
308
328
  | `adapter` | `Adapter` | - | Casbin adapter instance (e.g. `ScopedCasbinAdapter`) |
309
- | `isScoped` | `boolean` | `false` | Enables the scoped model: 4-token `(sub, dom, obj, act)` requests; auto-registers `keyMatch` on `g` + `objectMatch` on the resource relation |
329
+ | `isScoped` | `boolean` | `false` | Enables the scoped model: 4-token `(sub, dom, obj, act)` requests. Auto-registers `keyMatch`, `objectMatch`, and `ResourceRoleManager` - see [`CasbinAuthorizationEnforcer`](#casbinauthorizationenforcer) |
310
330
  | `poolSize` | `number` | `16` | Pooled enforcers (each request enforces on its own borrowed instance) |
311
331
  | `poolAcquireTimeoutMs` | `number` | `5000` | Max ms to wait for a free pooled enforcer before failing closed |
312
332
  | `normalizePayloadFn` | `(opts) => { subject, resource, action, domain? }` | - | Custom (non-scoped) payload normalizer, run before evaluation |
@@ -330,6 +350,15 @@ interface ICasbinEnforcerOptions<E extends Env = Env, TAction = string, TResourc
330
350
  > [!NOTE]
331
351
  > `cached.options.expiresIn` must be `>= 10_000` ms (`MIN_EXPIRES_IN`). Caching is **Redis-only** - the in-memory driver was removed.
332
352
 
353
+ ### Domain hierarchy edges (`g3`)
354
+
355
+ There is no enforcer-level option for domain hierarchy - a role assignment (`g`), a grant (`g3`), or a domain membership (`g2`) declared at a parent domain reaching its children is driven entirely by `g3` policy lines already present in a principal's own line set, whenever `isScoped: true`. Two sources produce those lines - see `resolveDomainEdges` on [`ScopedCasbinAdapter`](#scopedcasbinadapter) below for the second one:
356
+
357
+ - `domain_inherits` rows reachable from the principal's domain closure (`ScopedCasbinAdapter`'s `DOMAIN_EDGE` branch, always on).
358
+ - `ScopedCasbinAdapter`'s `resolveDomainEdges` constructor hook, for a hierarchy the app already owns on a business table.
359
+
360
+ `registerMatchers()` (see [`configure()`](#configure) below) wires this unconditionally for every scoped model, sharing one overlay across the three role managers. Freshness is whatever the per-user policy-line cache already guarantees; there is no separate TTL or invalidation call to reason about.
361
+
333
362
  **Cache configuration (discriminated union):**
334
363
 
335
364
  ```typescript
@@ -381,7 +410,9 @@ type TAuthorizationDomainResolver<E extends Env = Env> = (opts: {
381
410
  }) => ValueOrPromise<TNullable<{ type: string; id: IdType }>>;
382
411
  ```
383
412
 
384
- `resolveRequestDomain()` (`common/resolve-request-domain.ts`) turns either shape into a casbin domain string with this precedence: `spec.domain` (resolver, then declarative `readDeclarative()`) -> `IAuthorizeOptions.domainResolver` -> `AuthorizationDomainScopes.SYSTEM_WIDE`. `readDeclarative()` reads `context.req.param/header/query()` for `'param'|'header'|'query'`, or `context.get(key)` for `'context'`.
413
+ `resolveRequestDomain()` (`providers/request-domain.ts`) turns either shape into a casbin domain string. Precedence: `spec.domain` (resolver, then declarative `readDeclarative()`) -> `IAuthorizeOptions.domainResolver` -> `AuthorizationDomainScopes.SYSTEM_WIDE`.
414
+
415
+ `readDeclarative()` reads `context.req.param/header/query()` for `'param'|'header'|'query'`, or `context.get(key)` for `'context'`.
385
416
 
386
417
  ### TAuthorizationConditions / TAuthorizationVoter / TAuthorizeFn
387
418
 
@@ -413,7 +444,7 @@ interface IAuthorizationRequest<TAction = string, TResource = string> {
413
444
  }
414
445
  ```
415
446
 
416
- Source -> [`common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/common/types.ts)
447
+ Source -> [`common/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/common/types.ts)
417
448
 
418
449
  ## Constants
419
450
 
@@ -428,13 +459,15 @@ All constant classes follow the same pattern: static readonly values + `SCHEME_S
428
459
  | `Authorization.ENFORCER` | `'authorization.enforcer'` | Binding key prefix for enforcers |
429
460
  | `Authorization.DOMAIN` | `'authorization.domain'` | Context key for the resolved request domain scope |
430
461
 
431
- **`AuthorizationActions`** - `CREATE` `UPDATE` `DELETE` `EXECUTE` `READ` `WRITE` `MANAGE`. `AuthorizationActions.LATTICE` declares the standard action hierarchy consumed by `AuthorizationPolicyBuilder.actionLattice()`:
462
+ **`AuthorizationActions`** - `CREATE` `UPDATE` `DELETE` `EXECUTE` `READ` `WRITE` `MANAGE` `CUSTOM`. `AuthorizationActions.LATTICE` declares the standard action hierarchy consumed by `AuthorizationPolicyBuilder.actionLattice()`:
432
463
 
433
464
  | `child` | `parent` |
434
465
  |---|---|
435
466
  | `READ`, `WRITE`, `EXECUTE` | `MANAGE` |
436
467
  | `CREATE`, `UPDATE`, `DELETE` | `WRITE` |
437
468
 
469
+ `CUSTOM` (`'custom'`) is a grant-mode marker for a subset grant carrying `metadata.ops` - see [Subset grants](#subset-grants-custom-rows). It is deliberately absent from `LATTICE`: it names an encoding, not a position in the action hierarchy.
470
+
438
471
  **`AuthorizationDecisions`** - `ALLOW` `DENY` `ABSTAIN`.
439
472
 
440
473
  | Method | String check | Number check |
@@ -460,7 +493,13 @@ All constant classes follow the same pattern: static readonly values + `SCHEME_S
460
493
  | `REGEX_MATCH` | `'regexMatch'` | Treats the stored/policy value as a full regular expression |
461
494
 
462
495
  > [!IMPORTANT]
463
- > Applied as `fn(requestDomain, policyDomain)` - the wildcard must live on the **stored/policy** side. `keyMatch("Merchant_X", "*") === true`, `keyMatch("Merchant_X", "Merchant_X") === true`, `keyMatch("Merchant_X", "Merchant_Y") === false`.
496
+ > Applied as `fn(requestDomain, policyDomain)` - the wildcard must live on the **stored/policy** side.
497
+ >
498
+ > | Call | Result |
499
+ > |---|---|
500
+ > | `keyMatch("Merchant_X", "*")` | `true` |
501
+ > | `keyMatch("Merchant_X", "Merchant_X")` | `true` |
502
+ > | `keyMatch("Merchant_X", "Merchant_Y")` | `false` |
464
503
 
465
504
  **`CasbinRuleVariants`** - the Casbin line prefixes declared by the scoped model, numbered in request-tuple order (`sub -> dom -> obj -> act`).
466
505
 
@@ -470,7 +509,7 @@ All constant classes follow the same pattern: static readonly values + `SCHEME_S
470
509
  | `G` | `'g'` | Role membership + role inheritance (the `sub` axis) |
471
510
  | `G2` | `'g2'` | User -> domain membership (the `dom` axis) |
472
511
  | `G3` | `'g3'` | Domain hierarchy (the `dom` axis) |
473
- | `G4` | `'g4'` | Resource hierarchy (the `obj` axis, via `objectMatch`) |
512
+ | `G4` | `'g4'` | Resource hierarchy (the `obj` axis, served by `ResourceRoleManager`) |
474
513
  | `G5` | `'g5'` | Action hierarchy (the `act` axis) |
475
514
 
476
515
  **`AuthorizationPolicyVariants`** - the DB `variant` discriminator stored on each `PolicyDefinition` row (the kind of "edge"). Each entry carries `action` (the DB value) and `rule` (the Casbin prefix `ScopedCasbinAdapter` emits for it).
@@ -504,7 +543,7 @@ All constant classes follow the same pattern: static readonly values + `SCHEME_S
504
543
  | `GUEST` | `'001_guest'` | 1 |
505
544
  | `UNKNOWN_USER` | `'000_unknown-user'` | 0 |
506
545
 
507
- Source -> [`common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/common/constants.ts)
546
+ Source -> [`common/constants.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/common/constants.ts)
508
547
 
509
548
  ## CASBIN_RBAC_DOMAIN_SCOPED_MODEL
510
549
 
@@ -536,17 +575,23 @@ m = g(r.sub, p.sub, r.dom) && (p.dom == "SYSTEM_WIDE" || (p.dom == "ANY_MEMBER"
536
575
  | `g` | `sub` | `assign_role` (user -> role) + `role_inherits` (role -> role), domain-aware. Registered with `keyMatch` so a `*` domain on a link matches any request domain |
537
576
  | `g2` | `dom` (membership) | `join_domain` - powers the `ANY_MEMBER` grant scope |
538
577
  | `g3` | `dom` (nesting) | `domain_inherits`, plus a self-link so an exact domain always matches itself |
539
- | `g4` | `obj` | `resource_inherits` - explicit non-standard nesting edges; registered with `objectMatch` |
578
+ | `g4` | `obj` | `resource_inherits` - explicit non-standard nesting edges; served by `ResourceRoleManager`, not a matching function |
540
579
  | `g5` | `act` | `action_inherits`, plus a self-link |
541
580
 
542
- **Effect** is casbin's `allow-and-deny` effector: a request needs a matching `allow` AND no matching `deny` - default-DENY, and an explicit `deny` always overrides an `allow`. This is deliberately NOT casbin's `deny-override` effector (`!some(where (p.eft == deny))`), which would be default-ALLOW.
581
+ **Effect** is casbin's `allow-and-deny` effector. A request needs a matching `allow` AND no matching `deny` - default-DENY. An explicit `deny` always overrides an `allow`. This is deliberately NOT casbin's `deny-override` effector (`!some(where (p.eft == deny))`), which would be default-ALLOW.
582
+
583
+ **Domain clause**, matched by `p.dom`:
543
584
 
544
- **Domain clause**, matched by `p.dom`: `SYSTEM_WIDE` matches every domain (bypasses membership, super-admin); `ANY_MEMBER` matches every domain the subject joined (via `g2`); `<Type>_<id>` matches that domain, or a nested child via `g3`.
585
+ | `p.dom` value | Matches |
586
+ |---|---|
587
+ | `SYSTEM_WIDE` | Every domain - bypasses membership, super-admin |
588
+ | `ANY_MEMBER` | Every domain the subject joined, via `g2` |
589
+ | `<Type>_<id>` | That domain, or a nested child via `g3` |
545
590
 
546
591
  > [!NOTE]
547
592
  > Relies on the default `DefaultRoleManager`'s self-link behavior (`hasLink(name, name) === true`) for `g3`/`g4`/`g5` - a custom role manager must preserve self-links.
548
593
 
549
- Source -> [`enforcers/models/rbac-domain.model.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/enforcers/models/rbac-domain.model.ts)
594
+ Source -> [`enforcers/models/rbac-domain.model.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/enforcers/models/rbac-domain.model.ts)
550
595
 
551
596
  ## AbstractAuthRegistry
552
597
 
@@ -579,7 +624,7 @@ type TRegistryDescriptor<TItem> = { container: Container; targetClass: TClass<TI
579
624
 
580
625
  `AuthorizationEnforcerRegistry.getBindingPrefix()` returns `Authorization.ENFORCER` (`'authorization.enforcer'`).
581
626
 
582
- Source -> [`base/abstract-auth-registry.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/base/abstract-auth-registry.ts)
627
+ Source -> [`base/abstract-auth-registry.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/base/abstract-auth-registry.ts)
583
628
 
584
629
  ## AuthorizationEnforcerRegistry
585
630
 
@@ -614,7 +659,7 @@ class AuthorizationEnforcerRegistry extends AbstractAuthRegistry<IAuthorizationE
614
659
  |--------|---------|-------------|
615
660
  | `getInstance()` | `AuthorizationEnforcerRegistry` | Singleton instance (created on first call) |
616
661
  | `register(opts)` | `this` | Registers enforcers with type-safe options (chainable) |
617
- | `hasEnforcers()` | `boolean` | `descriptors.size > 0` - used by the middleware to skip authorization when no enforcers exist |
662
+ | `hasEnforcers()` | `boolean` | `descriptors.size > 0` - used by the middleware to decide whether to honor `defaultDecision` instead of resolving an enforcer |
618
663
  | `getDefaultEnforcerName()` | `string` | Delegates to `getDefaultName()` |
619
664
  | `resolveEnforcer({ name })` | `Promise<IAuthorizationEnforcer>` | Resolves + auto-configures once (`configuredEnforcers` Set) |
620
665
  | `resolveOptions()` | `IAuthorizeOptions \| undefined` | Iterates all registered containers looking for `AuthorizeBindingKeys.OPTIONS` |
@@ -622,7 +667,12 @@ class AuthorizationEnforcerRegistry extends AbstractAuthRegistry<IAuthorizationE
622
667
  | `rebuildUserCache(opts)` | `Promise<{ cacheKey, lineCount }>` | Drops then immediately re-extracts + re-caches |
623
668
  | `reset()` | `void` | Clears descriptors AND `configuredEnforcers` |
624
669
 
625
- **`register()` behavior:** validates no duplicate names within the call, validates each name is not already registered, binds each class as a singleton at `authorization.enforcer.{name}`, and - if `options` is given - binds it to `AuthorizeBindingKeys.enforcerOptions(name)`.
670
+ **`register()` behavior:**
671
+
672
+ - Validates no duplicate names within the call.
673
+ - Validates each name is not already registered.
674
+ - Binds each class as a singleton at `authorization.enforcer.{name}`.
675
+ - If `options` is given, binds it to `AuthorizeBindingKeys.enforcerOptions(name)`.
626
676
 
627
677
  **Configure-once pattern:**
628
678
 
@@ -637,7 +687,7 @@ async resolveEnforcer(opts: { name: string }): Promise<IAuthorizationEnforcer> {
637
687
  }
638
688
  ```
639
689
 
640
- Source -> [`enforcers/enforcer-registry.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/enforcers/enforcer-registry.ts)
690
+ Source -> [`enforcers/enforcer-registry.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/enforcers/enforcer-registry.ts)
641
691
 
642
692
  ## IAuthorizationEnforcer interface
643
693
 
@@ -675,7 +725,7 @@ interface IAuthorizationEnforcer<
675
725
 
676
726
  ## CasbinAuthorizationEnforcer
677
727
 
678
- Wraps the `casbin` library (optional peer dependency). Each request evaluates on its **own** enforcer borrowed from a `BasePoolHelper<Enforcer>` - the adapter (DB load) only runs on a throwaway enforcer to build a user's lines (cached in Redis if configured); every request then enforces on a pooled enforcer freshly loaded with those lines. This isolates concurrency and keeps the DB out of the hot path.
728
+ Wraps the `casbin` library (optional peer dependency). The adapter only loads from the database on a throwaway enforcer, to build one user's policy lines (cached in Redis if configured). Every request then evaluates on its own enforcer, borrowed from a `BasePoolHelper<Enforcer>` and freshly loaded with those lines. This isolates concurrency and keeps the database out of the hot path.
679
729
 
680
730
  ```typescript
681
731
  class CasbinAuthorizationEnforcer<E extends Env = Env, TAction extends string = string, TResource extends string = string>
@@ -723,16 +773,41 @@ Called once by the registry on first use:
723
773
  5. Builds a `BasePoolHelper<Enforcer>` (`size = poolSize ?? 16`, `acquireTimeoutMs = poolAcquireTimeoutMs ?? 5000`). Each pooled enforcer is created **without an adapter** (no DB load at warmup), then `registerMatchers()` and `assertMatcherCompilesSync()` run on it.
724
774
  6. `await pool.warmup()` - pre-creates the enforcers.
725
775
 
726
- `registerMatchers()` - when `isScoped`, registers `keyMatch` as the domain matching func on `g`, adds `objectMatch` as a function, and registers it as the matching func on the resource relation (`g4`). When `domainMatching` is set (flat model), registers the chosen `Util.*Func` on the named role definition. Always finishes with `buildRoleLinks()`.
776
+ **`registerMatchers()`** - when `isScoped`, registers three things:
777
+
778
+ | Registers | On |
779
+ |---|---|
780
+ | `keyMatch` | Domain matching func on `g` |
781
+ | `objectMatch` | Matcher-expression function via `addFunction` - called directly in the model's matcher string, not as a relation's matching func |
782
+ | `ResourceRoleManager` | Named role manager for `g4` |
783
+
784
+ `g4` skips `addNamedMatchingFunc` on purpose. It sets casbin's `hasPattern`, which disables `DefaultRoleManager`'s fast path on every link check, not just `g4` lookups.
785
+
786
+ For every scoped model, three more role managers are wired unconditionally - not behind a separate option:
727
787
 
728
- `assertMatcherCompilesSync()` - a boot-time smoke test: forces casbin's lazy matcher compile with one dummy `enforceSync` (4 args when scoped/`normalizePayloadFn`, else 3), so a malformed matcher, an unregistered function, or an arity mismatch fails at warmup instead of on the first real request.
788
+ | Registers | On | Notes |
789
+ |---|---|---|
790
+ | `MembershipRoleManager` | `g2` | Joining a parent domain membership makes the request domain's ancestors match too |
791
+ | `DomainHierarchyRoleManager` | `g3` | Grant domain nesting, request-domain-first |
792
+ | `DomainHierarchyRoleManager` (`reversed: true`) | `g`, via casbin's own `DefaultRoleManager.addDomainHierarchy()` | Role-assignment domain, stored-domain-first - the opposite argument order from `g3` |
793
+
794
+ The `g3` instance, the reversed `g` instance and `MembershipRoleManager` on `g2` are handed the same overlay `Map<child, Set<parent>>`. Casbin's `buildRoleLinks()` feeds every `g3` policy line to the `g3` instance via `addLink`, which writes into that shared overlay - the reversed `g` instance and `MembershipRoleManager` only ever read it, since casbin never puts the `g`-axis manager in its own `rmMap` and so never calls `addLink` on it directly. See [Domain hierarchy edges (`g3`)](#domain-hierarchy-edges-g3) above for where those `g3` lines come from.
795
+
796
+ When `domainMatching` is set (flat model), `registerMatchers()` registers the chosen `Util.*Func` on the named role definition instead, and always finishes with `buildRoleLinks()`.
797
+
798
+ **`assertMatcherCompilesSync()`** is a boot-time smoke test. It forces casbin's lazy matcher compile with one dummy `enforceSync` call (4 args when scoped/`normalizePayloadFn`, else 3). A malformed matcher, an unregistered function, or an arity mismatch fails at warmup, not on the first real request.
729
799
 
730
800
  ### buildRules()
731
801
 
732
802
  Returns `ICasbinRules = { user, lines }` - the user's complete Casbin policy lines.
733
803
 
734
- - `extractUserLines(user)` builds a fresh, isolated enforcer *with the adapter*, calls `adapter.loadFilteredPolicy({ principal: { type, id } })`, then `extractLinesFrom()` serializes every p-type and g-type rule (not just `p`/`g` - every `p*`/`g*` the model declares, so the scoped model's `g2`-`g5` hierarchies are included) back into lines.
735
- - `fetchLinesWithRedisCache` returns cached lines on a hit (Redis owns expiry via `PX`). On a miss it dedups concurrent misses through `pendingLineFetches` (single-flight), extracts once, and writes the lines back to Redis. A corrupt entry is logged and discarded (refetch), never a 500.
804
+ | Function | Behavior |
805
+ |---|---|
806
+ | `extractUserLines(user)` | Builds a fresh, isolated enforcer *with the adapter* and calls `adapter.loadFilteredPolicy({ principal: { type, id } })` |
807
+ | `extractLinesFrom()` | Serializes every `p*`/`g*` rule the model declares back into lines, not just `p`/`g` - including the scoped model's `g2`-`g5` hierarchies |
808
+ | `fetchLinesWithRedisCache` | Returns cached lines on a hit (Redis owns expiry via `PX`); on a miss, dedups concurrent misses via `pendingLineFetches` (single-flight), extracts once, and writes the lines back to Redis |
809
+
810
+ A corrupt cache entry is logged and discarded, then refetched - never surfaced as a `500`.
736
811
 
737
812
  ### evaluate()
738
813
 
@@ -748,9 +823,9 @@ On any error inside `pool.use`, the pool **destroys** the borrowed enforcer (fai
748
823
 
749
824
  ### invalidateUserCache() / rebuildUserCache()
750
825
 
751
- Redis-only (throw if caching is disabled). `invalidateUserCache` deletes the user's shared Redis key (next request rebuilds lazily). `rebuildUserCache` deletes then immediately re-extracts (on a throwaway enforcer) and re-caches. Because the key is shared in Redis, a single call is correct across instances.
826
+ Redis-only - both throw if caching is disabled. `invalidateUserCache` deletes the user's shared Redis key; the next request rebuilds lazily. `rebuildUserCache` deletes, then immediately re-extracts (on a throwaway enforcer) and re-caches. The key is shared in Redis, so one call is correct across every instance.
752
827
 
753
- Source -> [`enforcers/casbin.enforcer.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/enforcers/casbin.enforcer.ts)
828
+ Source -> [`enforcers/casbin.enforcer.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/enforcers/casbin.enforcer.ts)
754
829
 
755
830
  ## BaseFilteredAdapter
756
831
 
@@ -781,14 +856,19 @@ abstract class BaseFilteredAdapter<TFilter = ICasbinPolicyFilter> extends BaseHe
781
856
  interface ICasbinPolicyFilter { principal: { type: string; id: IdType }; }
782
857
 
783
858
  /** Minimal contract - NOT the framework's general IDataSource. Any Drizzle-backed datasource satisfies it. */
784
- interface ICasbinPolicySource { connector: TCasbinPolicyConnector; }
859
+ interface ICasbinPolicySource {
860
+ getConnector?(): TCasbinPolicyConnector; // preferred: lazily wires the driver on first read, survives pool rotation
861
+ connector?: TCasbinPolicyConnector; // back-compat: a pre-wired connector
862
+ }
785
863
  type TCasbinPolicyConnector = PgDatabase<PgQueryResultHKT, Record<string, AnyType>>;
786
864
  ```
787
865
 
788
866
  > [!NOTE]
789
- > Components that only need query execution depend on this minimal local contract (`ICasbinPolicySource`) rather than a connector class - `src/components/**` never imports `@/connectors/postgres` for this.
867
+ > `ICasbinPolicySource` is a minimal local contract, not the framework's general `IDataSource` - `src/components/**` never imports `@/connectors/postgres` for this. The `connector` getter resolves `getConnector?.() ?? connector`. When a datasource exposes neither, it throws a clear `[BaseFilteredAdapter]` error - never a bare `TypeError`.
868
+
869
+ `query()` runs a raw `SQL` statement and normalizes the result to a row array. Drizzle's `execute()` shape differs per driver: node-postgres yields `{ rows }`, postgres-js yields the row list itself. Call `query()` rather than read `.rows` directly.
790
870
 
791
- `query()` runs a raw `SQL` statement and normalizes the result to a row array - Drizzle's `execute()` shape differs per driver (node-postgres yields `{ rows }`, postgres-js yields the row list itself), so subclasses must call `query()` rather than read `.rows` directly. `loadLines()` is the other orchestration helper - subclasses call it after assembling their own casbin lines:
871
+ `loadLines()` is the other orchestration helper. Call it after assembling your own casbin lines:
792
872
 
793
873
  ```typescript
794
874
  protected async loadLines(opts: { model: Model; lines: string[] }): Promise<void> {
@@ -799,7 +879,7 @@ protected async loadLines(opts: { model: Model; lines: string[] }): Promise<void
799
879
  }
800
880
  ```
801
881
 
802
- Source -> [`adapters/base-filtered.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/adapters/base-filtered.ts), [`adapters/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/adapters/types.ts)
882
+ Source -> [`adapters/base-filtered.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/adapters/base-filtered.ts), [`adapters/types.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/adapters/types.ts)
803
883
 
804
884
  ## ScopedCasbinAdapter
805
885
 
@@ -808,27 +888,76 @@ The generic, read-only `FilteredAdapter` for the scoped RBAC model. Reads **one
808
888
  ```typescript
809
889
  class ScopedCasbinAdapter extends BaseFilteredAdapter<IScopedCasbinPolicyFilter> {
810
890
  protected readonly entities: IScopedCasbinEntities;
811
- constructor(opts: { dataSource: ICasbinPolicySource; entities: IScopedCasbinEntities });
891
+ protected readonly resolveDomainEdges?: TResolveDomainEdgesFn;
892
+ constructor(opts: {
893
+ dataSource: ICasbinPolicySource;
894
+ entities: IScopedCasbinEntities;
895
+ resolveDomainEdges?: TResolveDomainEdgesFn;
896
+ });
812
897
 
813
898
  async loadFilteredPolicy(model: Model, filter: IScopedCasbinPolicyFilter): Promise<void>;
814
899
 
815
- protected queryRoleAssignments(opts): Promise<{ lines: string[]; roleIds: IdType[] }>; // -> g
816
- protected queryMemberships(opts): Promise<string[]>; // -> g2
817
- protected queryGrants(opts): Promise<string[]>; // -> p
818
- protected loadStructuralTrees(): Promise<string[]>; // role(g)/domain(g3)/resource(g4)/action(g5)
819
- protected queryRoleInherits(): Promise<string[]>; // -> g
820
- protected queryDomainInherits(): Promise<string[]>; // -> g3
821
- protected queryResourceInherits(): Promise<string[]>; // -> g4
822
- protected queryActionInherits(): Promise<string[]>; // -> g5
823
- protected expandRoleClosure(opts: { role: { ids: IdType[]; edges: string[] } }): IdType[]; // BFS over role_inherits
900
+ protected async queryPrincipalPolicies(opts: {
901
+ principal: { type: string; id: IdType };
902
+ }): Promise<TPrincipalPolicyRow[]>; // one statement, two recursive CTEs (role_closure, domain_closure) - direct edges + reachable role_inherits + role-closure grants + reachable domain_inherits
903
+ protected collectDirectRow(opts: {
904
+ row: TPrincipalPolicyRow;
905
+ principal: { type: string; id: IdType };
906
+ lines: string[];
907
+ directGrants: TGrantRow[];
908
+ }): void; // routes one 'direct' row to its g/g2 line, or into the direct-grant batch
909
+ protected async buildGrantLines(opts: { subjectType: string; rows: TGrantRow[] }): Promise<string[]>; // -> p, shared by direct and role-closure grants
910
+ protected async queryEdgePolicies(): Promise<string[]>; // -> g4 (resource_inherits) + g5 (action_inherits), the two code-fixed structural trees
824
911
  }
825
912
  ```
826
913
 
914
+ ```typescript
915
+ /** The `kind` discriminator values in `TPrincipalPolicyRow`, one per UNION ALL branch of `queryPrincipalPolicies`. */
916
+ class PrincipalPolicyEdges {
917
+ static readonly DIRECT = 'direct';
918
+ static readonly ROLE_EDGE = 'roleEdge';
919
+ static readonly ROLE_GRANT = 'roleGrant';
920
+ static readonly DOMAIN_EDGE = 'domainEdge';
921
+ }
922
+
923
+ /** A grant row as fetched, before it becomes casbin lines. Permission columns are null when the join misses. */
924
+ type TGrantRow = {
925
+ subjectId: IdType;
926
+ objectCode: TNullable<string>;
927
+ objectSubject: TNullable<string>;
928
+ objectMethod: TNullable<string>;
929
+ action: TNullable<string>;
930
+ effect: TNullable<string>;
931
+ domain: TNullable<string>;
932
+ metadata?: unknown;
933
+ };
934
+
935
+ /** A row from the single principal-policy statement; `kind` says which branch produced it. */
936
+ type TPrincipalPolicyRow = TGrantRow & {
937
+ kind: TConstValue<typeof PrincipalPolicyEdges>;
938
+ variant: string;
939
+ targetType: TNullable<string>;
940
+ targetId: IdType;
941
+ };
942
+ ```
943
+
944
+ > [!NOTE]
945
+ > **There is no cache in the adapter.** Every `loadFilteredPolicy()` call re-runs both statements. If extraction cost becomes a measured problem, add the indexes below rather than a staleness window. The framework does not create these indexes. Your `PolicyDefinition` schema owns that, but the queries need them:
946
+ >
947
+ > | Index | Serves |
948
+ > |---|---|
949
+ > | `(variant, subject_type, subject_id)` | `queryPrincipalPolicies`' two CTE anchor terms (`role_closure`, `domain_closure`) and its `direct` branch |
950
+ > | `(variant, subject_id)` | `role_closure`'s recursive-term join and the role-grant branch |
951
+ > | `(variant, subject_type, subject_id)` again | `domain_closure`'s recursive term and the `domainEdge` branch. A domain node's identity is `(type, id)`, so both join on the pair, not `subject_id` alone |
952
+ > | `(variant)`, or per-variant partial indexes | `queryEdgePolicies`' two branches (`resource_inherits`, `action_inherits`) - each filters on `variant` alone |
953
+ >
954
+ > Without them, the recursive CTEs' anchor and direct-edge branches fall back to a sequential scan of the whole `PolicyDefinition` table. Measured with `EXPLAIN (ANALYZE, BUFFERS)` against a real Postgres database.
955
+
827
956
  ```typescript
828
957
  interface IScopedCasbinTable { tableName: string; schemaName?: string; }
829
958
 
830
959
  interface IScopedCasbinEntities {
831
- policyDefinition: IScopedCasbinTable; // the single edge table
960
+ policyDefinition: IScopedCasbinTable & { metadata?: { columnName: string } }; // metadata.columnName opts into subset grants
832
961
  permission: IScopedCasbinTable; // permission catalog (id, code, ...)
833
962
  principals: { user: string; role: string }; // casbin name prefixes
834
963
  domainTypes: string[]; // e.g. ['Merchant', 'Organizer']
@@ -838,14 +967,47 @@ interface IScopedCasbinEntities {
838
967
  interface IScopedCasbinPolicyFilter { principal: { type: string; id: IdType }; }
839
968
  ```
840
969
 
841
- **`loadFilteredPolicy()` - two waves:**
970
+ **`loadFilteredPolicy()` runs one wave of two independent statements:**
971
+
972
+ 1. `Promise.all` of two statements, neither waiting on the other:
973
+
974
+ **`queryPrincipalPolicies`** covers everything scoped to the principal, tagged by `kind`:
975
+
976
+ | `kind` | Rows |
977
+ |---|---|
978
+ | `DIRECT` | The principal's own `assign_role` / `join_domain` / `grant` rows |
979
+ | `ROLE_EDGE` | `role_inherits` edges reachable from its roles |
980
+ | `ROLE_GRANT` | Grants of that role closure |
981
+ | `DOMAIN_EDGE` | `domain_inherits` edges reachable from its domains |
982
+
983
+ It resolves two `WITH RECURSIVE` CTEs in SQL. `role_closure` seeds from `assign_role` rows and walks `role_inherits`. `domain_closure` seeds from `join_domain` rows and walks `domain_inherits`. Each recursive term uses `UNION`, not `UNION ALL` - the de-duplication is what terminates a cyclic graph.
984
+
985
+ **`queryEdgePolicies`** covers the two code-fixed structural trees, `resource_inherits` (`g4`) and `action_inherits` (`g5`), merged into one statement with two `UNION ALL` branches. `domain_inherits` (`g3`) is not loaded here - see [why `g3` is scoped differently](#why-g3-is-scoped-and-g4-g5-are-not).
986
+
987
+ 2. **Row routing:**
988
+
989
+ | `kind` | Routed to |
990
+ |---|---|
991
+ | `direct` | `collectDirectRow` - `g` for `assign_role`, `g2` for `join_domain`, or the direct-grant batch for `grant` |
992
+ | `roleEdge` | `g` lines, inline |
993
+ | `domainEdge` | `g3` lines, inline |
994
+ | `roleGrant` | Batched separately |
995
+
996
+ Both grant batches expand through the shared `buildGrantLines`.
997
+
998
+ 3. All lines load via `loadLines`.
999
+
1000
+ Only reachable edges are emitted: `role_inherits` edges from the principal's roles, and `domain_inherits` edges from its domains - never the whole role/domain graph. An edge outside either closure could never be traversed by the matcher anyway. This is behavior-preserving, and it shrinks every user's payload.
842
1001
 
843
- 1. **Wave 1 (parallel):** the principal's own edges - role assignments (`g`), domain memberships (`g2`), direct grants (`p`) - plus the shared structural trees (`role_inherits` -> `g`, `domain_inherits` -> `g3`, `resource_inherits` -> `g4`, `action_inherits` -> `g5`).
844
- 2. **Role closure:** `expandRoleClosure` does a cycle-safe BFS over the `role_inherits` (`g`) edges to collect the assigned roles plus all transitive parents.
845
- 3. **Wave 2:** fetches the grants (`p`) of every role in the closure, so a user inherits parent-role permissions.
846
- 4. All lines load via `loadLines`.
1002
+ ### Why `g3` is scoped and `g4`/`g5` are not
847
1003
 
848
- All queries use the `sql` template tag from `drizzle-orm`; tables are schema-qualified via `sql.identifier` (injection-safe), interpolated values are bound parameters. The soft-delete clause (`AND <alias>.<col> IS NULL`) is appended when `entities.softDelete.use` is true. `queryGrants` short-circuits to `[]` when given no subject ids (no DB round-trip).
1004
+ `g4` (resource) and `g5` (action) are fixed by the codebase - a few hundred rows, constant regardless of tenant count. `queryEdgePolicies` loads them whole for every principal.
1005
+
1006
+ `g3` (domain) grows with the domain count - many merchants under few organizers. It is scoped to the principal's domain closure inside `queryPrincipalPolicies` instead.
1007
+
1008
+ **The permission join is a `LEFT JOIN`, not `INNER JOIN`.** A grant whose target does not resolve (missing or soft-deleted `Permission` row) is logged and skipped by `buildGrantLines`, not silently dropped from the result set.
1009
+
1010
+ All queries use the `sql` template tag from `drizzle-orm`. Tables are schema-qualified via `sql.identifier` (injection-safe); interpolated values are bound parameters. The soft-delete clause (`AND <alias>.<col> IS NULL`) is appended when `entities.softDelete.use` is true.
849
1011
 
850
1012
  ```typescript
851
1013
  import { ScopedCasbinAdapter } from '@venizia/ignis';
@@ -862,23 +1024,86 @@ const adapter = new ScopedCasbinAdapter({
862
1024
  });
863
1025
  ```
864
1026
 
865
- Source -> [`adapters/scoped-casbin.adapter.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/adapters/scoped-casbin.adapter.ts)
1027
+ Source -> [`adapters/scoped-casbin.adapter.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/adapters/scoped-casbin.adapter.ts)
866
1028
 
867
- ## objectMatch
1029
+ ### `resolveDomainEdges` - `g3` edges from business data
868
1030
 
869
- Resource-hierarchy matcher registered by the scoped model (both as a function `objectMatch(r.obj, p.obj)` in the matcher expression AND as the `g4` matching func). Decides whether a requested resource falls under a granted one, WITHOUT needing a stored edge for the "standard" case (dotted nesting is derived from the code itself):
1031
+ A second, opt-in source of `g3` edges, alongside the `DOMAIN_EDGE` branch above. Configured on the constructor, for a tenant hierarchy the app already owns as a plain foreign key on a business table rather than `domain_inherits` rows:
870
1032
 
871
1033
  ```typescript
872
- const objectMatch = (requested: string, granted: string): boolean => {
873
- if (granted === '*') return true;
874
- if (requested === granted) return true;
875
- return requested.startsWith(`${granted}.`);
876
- };
1034
+ type TResolveDomainEdgesFn = (opts: {
1035
+ principal: { type: string; id: IdType };
1036
+ domains: string[];
1037
+ }) => Promise<Array<{ child: string; parent: string }>>;
1038
+
1039
+ new ScopedCasbinAdapter({
1040
+ dataSource,
1041
+ entities,
1042
+ resolveDomainEdges: async ({ principal, domains }) => [{ child, parent }, ...],
1043
+ });
877
1044
  ```
878
1045
 
879
- `objectMatch('Activation.findById', 'Activation')` -> `true` (dotted nesting - endpoint under subject). `objectMatch('OrderItem', 'Order')` -> `false` unless a `resource_inherits` (`g4`) edge links them - non-standard nesting always needs an explicit edge.
1046
+ `domains` is the principal's own domain closure, reconstructed from rows `queryPrincipalPolicies` already fetched (the `join_domain` seed plus both ends of every `domainEdge` row) rather than a third query. The hook returns `{ child, parent }` pairs as already-formed `<Type>_<id>` tokens; `loadFilteredPolicy` turns each into a `g3, <child>, <parent>` line, the exact shape a real `domain_inherits` row produces - nothing downstream can tell which source produced a given edge. A hook edge duplicating a real `domain_inherits` row is harmless: `DomainHierarchyRoleManager.addLink` stores parents in a `Set`, so the duplicate `addLink` call is a no-op.
1047
+
1048
+ A throwing hook is caught, logged, and treated as no edges for that one load - the rows already gathered (direct grants, role assignments, table-sourced `g3` rows) still load normally. This is the fail-secure direction: a missing `g3` edge only narrows what `g`/`g2`/`g3` reach, it can never widen it. The hook cannot join `queryPrincipalPolicies`'s wave (it needs that query's rows to compute `domains`) but does not wait on the independent `queryEdgePolicies` either - both resolve concurrently once the closure is known.
1049
+
1050
+ ### Subset grants (custom rows)
1051
+
1052
+ A grant row can express an arbitrary subset of a subject's operations instead of a full tier:
1053
+
1054
+ | Field | Value |
1055
+ |---|---|
1056
+ | `action` | `'custom'` |
1057
+ | Target | A subject-level resource node - `Permission.method` is `AuthorizationPermissionBuilder.RESOURCE_NODE_METHOD`, the `*` sentinel |
1058
+ | `metadata` | `{ ops: [...] }` |
1059
+
1060
+ `ops` holds **method names**, not full permission codes. The subject comes from the target node, so `ops: ['find']` against node `Order` resolves to `Order.find`.
1061
+
1062
+ `buildGrantLines` expands each custom row into one `p` line per operation, using that operation's catalogued action (never the `custom` sentinel). The emitted lines are byte-identical to what equivalent per-operation grant rows produce. Expansion runs one extra batched query (`queryOperationCatalog`) per extraction, and none when no custom rows are present.
880
1063
 
881
- Source -> [`common/object-match.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/common/object-match.ts)
1064
+ Reading is **opt-in**: without `entities.policyDefinition.metadata.columnName` mapped, the adapter never selects the `metadata` column, and a custom row is logged and skipped.
1065
+
1066
+ **Rejection rules** (`rejectCustomRow`, checked in this order). Each produces one `error`-level log line naming the subject id and object code, so a skipped grant can be diagnosed from the log alone:
1067
+
1068
+ | Condition | Logged reason |
1069
+ |---|---|
1070
+ | `action = 'custom'` but `metadata.columnName` is not mapped | `metadata.columnName is not mapped, so metadata.ops cannot be read` |
1071
+ | `action = 'custom'` but `metadata.ops` is missing, empty, or not an array of non-empty strings | `metadata.ops is missing, empty, or not an array of non-empty strings` |
1072
+ | `metadata.ops` is present but `action` is not `'custom'` | `metadata.ops is present but action is not "custom", so the intent is ambiguous` |
1073
+ | The target's `Permission.method` is not the `*` resource-node sentinel | `the target must be a subject-level resource node` |
1074
+
1075
+ A row that passes all four checks can still drop an individual **unresolvable operation name** during expansion. `expandCustomGrants` logs it separately, naming the unknown operations. The row's other valid operations still expand and emit lines.
1076
+
1077
+ **Composing a grant:** use `planGrant` (below) rather than hand-building a custom row. It collapses an operation selection into tier grants wherever possible. What does not collapse falls back to a custom row, or a single per-operation row.
1078
+
1079
+ Source -> [`adapters/scoped-casbin.adapter.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/adapters/scoped-casbin.adapter.ts)
1080
+
1081
+ ## AuthorizationPermissionBuilder.objectMatch
1082
+
1083
+ Resource-hierarchy matcher registered by the scoped model **only** as a function - `objectMatch(r.obj, p.obj)`, called directly in the matcher expression via `addFunction`. It decides whether a requested resource falls under a granted one, without needing a stored edge for the standard case. Dotted nesting is derived from the code itself.
1084
+
1085
+ `objectMatch` is **not** registered as the `g4` matching func. `g4` (`resource_inherits`) is served by a dedicated `ResourceRoleManager` instead - see [ScopedCasbinAdapter](#scopedcasbinadapter). Registering it via `addNamedMatchingFunc` would set casbin's `hasPattern`. That disables `DefaultRoleManager`'s O(1) fast path on every link check, not only `g4` lookups.
1086
+
1087
+ It lives as a `static` method on `AuthorizationPermissionBuilder`, the class that owns the `code = <subject>.<method>` format it matches against. It must stay `static` with no `this` reference, since Casbin calls it by reference:
1088
+
1089
+ ```typescript
1090
+ class AuthorizationPermissionBuilder {
1091
+ static objectMatch(requested: string, granted: string): boolean {
1092
+ if (granted === '*') return true;
1093
+ if (requested === granted) return true;
1094
+ return requested.startsWith(`${granted}.`);
1095
+ }
1096
+ }
1097
+
1098
+ enforcer.addFunction('objectMatch', AuthorizationPermissionBuilder.objectMatch);
1099
+ ```
1100
+
1101
+ | Call | Result | Why |
1102
+ |---|---|---|
1103
+ | `objectMatch('Activation.findById', 'Activation')` | `true` | Dotted nesting - endpoint under subject |
1104
+ | `objectMatch('OrderItem', 'Order')` | `false`, unless a `resource_inherits` (`g4`) edge links them | Non-standard nesting always needs an explicit edge |
1105
+
1106
+ Source -> [`builders/permission.builder.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/builders/permission.builder.ts)
882
1107
 
883
1108
  ## AuthorizationProvider
884
1109
 
@@ -918,8 +1143,11 @@ for (const voter of spec.voters ?? []) {
918
1143
  // ABSTAIN -> next voter
919
1144
  }
920
1145
 
921
- // 5. Resolve enforcer (no-enforcer fallback)
922
- if (!registry.hasEnforcers()) return next();
1146
+ // 5. Resolve enforcer (no-enforcer fallback honors defaultDecision - fails closed by default)
1147
+ if (!registry.hasEnforcers()) {
1148
+ if (options?.defaultDecision === 'allow') return next(); // logs a warning
1149
+ throw 403 'no enforcer registered'; // AuthorizationErrors.ENFORCER_NOT_REGISTERED
1150
+ }
923
1151
  const enforcer = await registry.resolveEnforcer({ name: enforcerName ?? registry.getDefaultEnforcerName() });
924
1152
 
925
1153
  // 5b. Resolve request domain - only when spec.domain or a global domainResolver is in play
@@ -949,7 +1177,7 @@ await next();
949
1177
  roles.map(r => typeof r === 'string' ? r : (r.identifier ?? r.name ?? String(r.id ?? '')));
950
1178
  ```
951
1179
 
952
- Source -> [`providers/authorization.provider.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/providers/authorization.provider.ts)
1180
+ Source -> [`providers/authorization.provider.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/providers/authorization.provider.ts)
953
1181
 
954
1182
  ## Standalone authorize() function
955
1183
 
@@ -962,7 +1190,7 @@ export const authorize = (opts: { spec: IAuthorizationSpec; enforcerName?: strin
962
1190
 
963
1191
  A module-level singleton `AuthorizationProvider`; the returned handler is a standard Hono `MiddlewareHandler`.
964
1192
 
965
- Source -> [`middlewares/authorize.middleware.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/middlewares/authorize.middleware.ts)
1193
+ Source -> [`middlewares/authorize.middleware.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/middlewares/authorize.middleware.ts)
966
1194
 
967
1195
  ## AuthorizationRole
968
1196
 
@@ -988,7 +1216,7 @@ class AuthorizationRole implements IAuthorizationRole {
988
1216
  interface IAuthorizationRole { readonly name: string; readonly priority: number; readonly identifier: string; }
989
1217
  ```
990
1218
 
991
- Source -> [`models/authorization-role.model.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/models/authorization-role.model.ts)
1219
+ Source -> [`models/authorization-role.model.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/models/authorization-role.model.ts)
992
1220
 
993
1221
  ## Policy and permission builders
994
1222
 
@@ -1003,6 +1231,8 @@ class AuthorizationPolicyBuilder {
1003
1231
  static readonly ACTION_PRINCIPAL = 'Action';
1004
1232
 
1005
1233
  static grant(opts: { subject: { type; id }; permission: { type; id }; action: string; domain?: TNullable<TPolicyDomainInput>; effect: TAuthorizationDecision }): PolicyDefinitionRow;
1234
+ /** A subset grant: subject -> resource node, granting only `ops` (method names). `action` is fixed to `AuthorizationActions.CUSTOM`; the adapter expands `ops` at read time (see Subset grants). */
1235
+ static customGrant(opts: { subject: { type; id }; permission: { type; id }; ops: string[]; domain?: TNullable<TPolicyDomainInput>; effect: TAuthorizationDecision }): PolicyDefinitionRow;
1006
1236
  static assignRole(opts: { user: { type; id }; role: { type; id }; domain?: TNullable<TPolicyDomainInput> }): PolicyDefinitionRow;
1007
1237
  static joinDomain(opts: { user: { type; id }; domain: { type; id } }): PolicyDefinitionRow;
1008
1238
  static roleInherits(opts: { child: { type; id }; parent: { type; id } }): PolicyDefinitionRow;
@@ -1026,7 +1256,7 @@ class AuthorizationPolicyBuilder {
1026
1256
 
1027
1257
  ### AuthorizationPermissionBuilder
1028
1258
 
1029
- Builds `Permission` catalog rows (the `obj` axis the scoped matcher resolves). Generic over the name/description type, so i18n and plain-text apps both fit - the framework only owns the `code`/`method`/`action` shape.
1259
+ Builds `Permission` catalog rows (the `obj` axis the scoped matcher resolves). Generic over the name/description type, so i18n and plain-text apps both fit. The framework only owns the `code`/`method`/`action` shape.
1030
1260
 
1031
1261
  ```typescript
1032
1262
  class AuthorizationPermissionBuilder {
@@ -1054,7 +1284,44 @@ class AuthorizationPermissionBuilder {
1054
1284
  }
1055
1285
  ```
1056
1286
 
1057
- Source -> [`common/policy-builder.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/common/policy-builder.ts), [`common/permission-builder.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/components/auth/authorize/common/permission-builder.ts)
1287
+ Source -> [`builders/policy.builder.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/builders/policy.builder.ts), [`builders/permission.builder.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/builders/permission.builder.ts)
1288
+
1289
+ ### GrantBuilder.planGrant
1290
+
1291
+ The supported way to compose an operations-subset grant. Pure - resolves against a caller-supplied catalog and returns rows for the caller to persist. It never touches the database. Call it on the singleton (`GrantBuilder.getInstance().planGrant(...)`).
1292
+
1293
+ ```typescript
1294
+ type TGrantIntent = { tier: TAuthorizationAction } | { ops: string[] };
1295
+
1296
+ class GrantBuilder {
1297
+ planGrant(opts: {
1298
+ subject: { type: string; id: IdType };
1299
+ resource: { type: string; id: IdType; subject: string };
1300
+ intent: TGrantIntent;
1301
+ catalog: Array<{ subject: string; method: string; code: string; action: string }>;
1302
+ domain?: TNullable<TPolicyDomainInput>;
1303
+ effect?: TAuthorizationDecision; // default AuthorizationDecisions.ALLOW
1304
+ supportsCustomMetadata?: boolean; // default true - false forces per-operation rows instead of a custom row
1305
+ exact?: boolean; // default false - true skips collapsing into tier grants
1306
+ }): TPlannedGrantRow[];
1307
+ }
1308
+ ```
1309
+
1310
+ | `intent` | Behavior |
1311
+ |---|---|
1312
+ | `{ tier }` | One tier-grant row. Throws on a tier outside `read`/`write`/`execute`/`manage`. |
1313
+ | `{ ops }` collapsing into `manage` | Only when the subject has an operation in **each** of `read`, `write`, and `execute` - otherwise `manage` would silently pre-authorize a future operation in an empty tier. |
1314
+ | `{ ops }` collapsing into a narrow tier | Whenever the selection **completely covers** a `read`/`write`/`execute` tier, however few operations it holds. `ops: ['find']` collapses into `read` when `find` is the subject's only `read` operation. |
1315
+ | `{ ops }` with `exact: true` | Skips collapsing entirely - use for a selection that must never widen as new operations join a tier. |
1316
+ | Leftover after collapsing | One custom row (`AuthorizationPolicyBuilder.customGrant`) - or, when the leftover is a single operation or `supportsCustomMetadata: false`, one per-operation row per leftover operation. |
1317
+
1318
+ A per-operation row's `permission.id` is the operation's **code** (e.g. `Order.find`), not a database id. The planner is pure and has no DB access, so the caller resolves codes to ids when persisting.
1319
+
1320
+ Throws (`getError`) on an invalid tier, an empty `ops`, or an `ops` entry absent from the resource's catalog slice.
1321
+
1322
+ Mirrors `ScopedCasbinAdapter.buildGrantLines`'s expansion: a planned custom row and the equivalent per-operation rows expand to identical casbin lines - see [Subset grants](#subset-grants-custom-rows).
1323
+
1324
+ Source -> [`builders/grant.builder.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/components/auth/authorize/builders/grant.builder.ts)
1058
1325
 
1059
1326
  ## Model-based authorization metadata
1060
1327
 
@@ -1092,7 +1359,7 @@ getAuthorizeModelSettings(opts: { format: 'array' }): Array<{ name: string; auth
1092
1359
  getAuthorizeModelSettings(opts: { format: 'record' }): Record<string, { authorize: IModelAuthorizeSettings; entry: IModelRegistryEntry }>;
1093
1360
  ```
1094
1361
 
1095
- Source -> [`base/metadata/persistents.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/metadata/persistents.ts), [`helpers/inversion/mixins/model.mixin.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/helpers/inversion/mixins/model.mixin.ts)
1362
+ Source -> [`base/metadata/persistents.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/base/metadata/persistents.ts), [`helpers/inversion/mixins/model.mixin.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/helpers/inversion/mixins/model.mixin.ts)
1096
1363
 
1097
1364
  ## Controller integration
1098
1365
 
@@ -1167,7 +1434,7 @@ type TRouteAuthConfig = { authenticate?: TRouteAuthenticateConfig; authorize?: T
1167
1434
 
1168
1435
  Applied identically to `count`, `find`, `findById`, `findOne`, `create`, `updateById`, `updateBy`, `deleteById`, `deleteBy`.
1169
1436
 
1170
- Source -> [`base/controllers/rest/abstract.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/controllers/rest/abstract.ts), [`base/controllers/grpc/abstract.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/controllers/grpc/abstract.ts), [`base/controllers/factory/definition.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core/src/base/controllers/factory/definition.ts)
1437
+ Source -> [`base/controllers/rest/abstract.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/base/controllers/rest/abstract.ts), [`base/controllers/grpc/abstract.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/base/controllers/grpc/abstract.ts), [`base/controllers/factory/definition.ts`](https://github.com/VENIZIA-AI/ignis/blob/main/packages/core-server/src/base/controllers/factory/definition.ts)
1171
1438
 
1172
1439
  ## Context variables
1173
1440
 
@@ -1180,7 +1447,7 @@ declare module 'hono' {
1180
1447
  [Authentication.AUDIT_USER_ID]: IdType;
1181
1448
  [Authentication.SKIP_AUTHENTICATION]: boolean;
1182
1449
 
1183
- [Authorization.RULES]: unknown;
1450
+ [Authorization.RULES]: Map<string, unknown>;
1184
1451
  [Authorization.SKIP_AUTHORIZATION]: boolean;
1185
1452
  [Authorization.DOMAIN]: string;
1186
1453
  }