@venizia/ignis-docs 0.0.8-1 → 0.0.8-2

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.
@@ -22,11 +22,9 @@ flowchart TD
22
22
  S5b -->|DI fails| E500c[/"500: Failed to resolve"/]
23
23
  S5b -->|OK| S6{"Step 6: Build rules"}
24
24
 
25
- S6 -->|No principalType| E400a[/"400: principalType required"/]
26
- S6 -->|Not initialized| E500d[/"500: Enforcer not initialized"/]
25
+ S6 -->|Not configured| E500d[/"500: Not configured. Call configure() first."/]
27
26
  S6 -->|No FilteredAdapter| E500e[/"500: Adapter does not support loadFilteredPolicy"/]
28
- S6 -->|Bad cache driver| E500f[/"500: Invalid cached.driver"/]
29
- S6 -->|Empty Redis key| E400b[/"400: Invalid cachedKey"/]
27
+ S6 -->|Empty Redis key| E400b[/"400: keyFn returned an empty cache key"/]
30
28
  S6 -->|OK| S7{"Step 7: Evaluate"}
31
29
 
32
30
  S7 -->|No action/resource| E500g[/"500: request.action and resource required"/]
@@ -73,22 +71,38 @@ All error messages from the authorization module, organized by source:
73
71
  |---------------|--------|--------|
74
72
  | `[CasbinAuthorizationEnforcer] "casbin" is not installed` | 500 | `configure` |
75
73
  | `[CasbinAuthorizationEnforcer] options.model is required.` | 500 | `configure` |
76
- | `[CasbinAuthorizationEnforcer] Enforcer not initialized. Call configure() first.` | 500 | `evaluate`, `buildRules` |
77
- | `[CasbinAuthorizationEnforcer] Adapter does not support loadFilteredPolicy.` | 500 | `buildRules` |
74
+ | `[CasbinAuthorizationEnforcer] Not configured. Call configure() first.` | 500 | `evaluate`, `loadPolicyLinesIntoModel` |
75
+ | `[extractUserLines] Adapter does not support loadFilteredPolicy.` | 500 | `buildRules` (via `extractUserLines`) |
78
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`) |
79
78
  | <code v-pre>[CasbinAuthorizationEnforcer] cached.options.expiresIn must be >= 10000 (ms) &#124; Received: {{value}}</code> | 500 | `configure` (via `validateExpiresIn`) |
80
- | <code v-pre>[buildRules] Invalid cached.driver &#124; Valids: [in-memory, redis]</code> | 500 | `buildRules` |
81
- | <code v-pre>[resolveCasbinEnforcer] Invalid cached.driver &#124; Valids: [in-memory, redis]</code> | 500 | `configure` (via `resolveCasbinEnforcer`) |
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
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.
83
88
 
84
89
  ### Policy Loading Errors (CasbinAuthorizationEnforcer internals)
85
90
 
86
91
  | Error Message | Status | Method |
87
92
  |---------------|--------|--------|
88
- | `[loadPoliciesWithRedisCache] Invalid cachedKey to start validate user authorization!` | 400 | `loadPoliciesWithRedisCache` |
89
- | `[loadPoliciesFromAdapter] Invalid state of enforcer` | 500 | `loadPoliciesFromAdapter` |
90
- | `[extractPolicyLines] Invalid state of enforcer` | 500 | `extractPolicyLines` |
91
- | `[loadPolicyLinesIntoModel] Enforcer not initialized. Call configure() first.` | 500 | `loadPolicyLinesIntoModel` |
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) |
92
106
 
93
107
  ## Troubleshooting
94
108
 
@@ -264,18 +278,18 @@ AuthorizationEnforcerRegistry.getInstance().register({
264
278
  });
265
279
  ```
266
280
 
267
- ### "[CasbinAuthorizationEnforcer] Adapter does not support loadFilteredPolicy"
281
+ ### "[extractUserLines] Adapter does not support loadFilteredPolicy"
268
282
 
269
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.
270
284
 
271
- **Fix:** Use an adapter that implements `FilteredAdapter`, such as `DrizzleCasbinAdapter` or a custom adapter extending `BaseFilteredAdapter`:
285
+ **Fix:** Use an adapter that implements `FilteredAdapter`, such as `ScopedCasbinAdapter` or a custom adapter extending `BaseFilteredAdapter`:
272
286
 
273
287
  ```typescript
274
- import { DrizzleCasbinAdapter } from '@venizia/ignis';
288
+ import { ScopedCasbinAdapter } from '@venizia/ignis';
275
289
 
276
- const adapter = new DrizzleCasbinAdapter({
290
+ const adapter = new ScopedCasbinAdapter({
277
291
  dataSource,
278
- entities: { ... },
292
+ entities: { /* IScopedCasbinEntities */ },
279
293
  });
280
294
  ```
281
295
 
@@ -297,9 +311,9 @@ cached: {
297
311
  },
298
312
  ```
299
313
 
300
- ### "[CasbinAuthorizationEnforcer] Enforcer not initialized"
314
+ ### "[CasbinAuthorizationEnforcer] Not configured. Call configure() first."
301
315
 
302
- **Cause:** The Casbin enforcer's `evaluate()`, `buildRules()`, or internal methods were called before `configure()`. This should not happen when using `resolveEnforcer()` (which auto-configures), but can occur if the enforcer is resolved manually via the DI container.
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.
303
317
 
304
318
  **Fix:** Always resolve enforcers through the registry's `resolveEnforcer()` method, which handles configure-once automatically:
305
319
 
@@ -323,36 +337,31 @@ keyFn: ({ user }) => {
323
337
  },
324
338
  ```
325
339
 
326
- ### "[loadPoliciesFromAdapter] Invalid state of enforcer"
327
-
328
- **Cause:** `loadPoliciesFromAdapter()` was called but the internal casbin enforcer is null. This is an internal state error.
329
-
330
- **Fix:** This should not occur in normal usage. If it does, ensure `configure()` completed successfully before any policy loading operations.
340
+ ### "[CasbinAuthorizationEnforcer] Not configured. Call configure() first."
331
341
 
332
- ### "[extractPolicyLines] Invalid state of enforcer"
342
+ **Cause:** `evaluate()` or `loadPolicyLinesIntoModel()` ran before the enforcer pool was built.
333
343
 
334
- **Cause:** `extractPolicyLines()` was called but the internal casbin enforcer is null. This method is called during Redis cache miss flow.
344
+ **Fix:** Ensure `configure()` completed successfully (the registry calls it on first use) before any
345
+ evaluation. The enforcer pool is created in `configure()`.
335
346
 
336
- **Fix:** Same as above -- ensure `configure()` completed before policy operations.
347
+ ### "[CasbinAuthorizationEnforcer] keyFn returned an empty cache key."
337
348
 
338
- ### "[loadPolicyLinesIntoModel] Enforcer not initialized"
349
+ **Cause:** The Redis `cached.options.keyFn` returned an empty string for a user.
339
350
 
340
- **Cause:** `loadPolicyLinesIntoModel()` was called but the internal casbin enforcer is null. This method is called during Redis cache hit flow to load cached lines.
351
+ **Fix:** Return a stable, non-empty key (commonly derived from `user.principalType` + `user.userId`):
341
352
 
342
- **Fix:** Same as above -- ensure `configure()` completed before policy operations.
353
+ ```typescript
354
+ keyFn: ({ user }) => `authz:policies:${user.principalType}:${user.userId}`,
355
+ ```
343
356
 
344
- ### Invalid driver errors
357
+ ### "[resolveModel] Invalid model.driver | Valids: [file, text]"
345
358
 
346
- **Messages:**
347
- - `[buildRules] Invalid cached.driver | Valids: [in-memory, redis]`
348
- - `[resolveCasbinEnforcer] Invalid cached.driver | Valids: [in-memory, redis]`
349
- - `[resolveModel] Invalid model.driver | Valids: [file, text]`
359
+ **Cause:** An unsupported `model.driver` string was passed.
350
360
 
351
- **Cause:** An unsupported driver string was passed in the options.
361
+ **Fix:** Use `CasbinEnforcerModelDrivers.FILE` (`'file'`) or `CasbinEnforcerModelDrivers.TEXT` (`'text'`).
352
362
 
353
- **Fix:** Use one of the valid values:
354
- - Cache drivers: `CasbinEnforcerCachedDrivers.IN_MEMORY` (`'in-memory'`) or `CasbinEnforcerCachedDrivers.REDIS` (`'redis'`)
355
- - Model drivers: `CasbinEnforcerModelDrivers.FILE` (`'file'`) or `CasbinEnforcerModelDrivers.TEXT` (`'text'`)
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**.
356
365
 
357
366
  ## Common Patterns
358
367
 
@@ -373,10 +382,10 @@ Check that rules are being cached correctly. The middleware caches on `Authoriza
373
382
 
374
383
  ### Casbin Policies Not Loading
375
384
 
376
- 1. **Check adapter entities** -- ensure `tableName` and `principalType` match your database schema
377
- 2. **Check variant column** -- policy rows must have `variant = 'policy'` (`CasbinRuleVariants.POLICY`), role assignments must have `variant = 'group'` (`CasbinRuleVariants.GROUP`)
378
- 3. **Check subject/target types** -- the SQL queries filter by `subject_type` and `target_type`
379
- 4. **Check the model file** -- ensure your `.conf` file matches the policy format (e.g., domain-aware vs simple)
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`
380
389
 
381
390
  ### Redis Cache Not Working
382
391
 
@@ -20,10 +20,8 @@
20
20
  | **AuthorizationProvider** | IProvider producing the `authorize()` middleware factory |
21
21
  | **authorize** | Standalone function wrapping `AuthorizationProvider.value()` |
22
22
  | **AuthorizationRole** | Value object for role identity with priority-based comparison |
23
- | **BaseFilteredAdapter** | Abstract casbin `FilteredAdapter` with template method pattern for query hooks |
24
- | **DrizzleCasbinAdapter** | Drizzle-based read-only `FilteredAdapter` using raw SQL queries |
25
- | **StringAuthorizationAction** | `IAuthorizationComparable` implementation for string actions (includes `WILDCARD = '*'`) |
26
- | **StringAuthorizationResource** | `IAuthorizationComparable` implementation for string resources |
23
+ | **BaseFilteredAdapter** | Thin abstract casbin `FilteredAdapter` (datasource plumbing + `loadLines`); subclasses implement only `loadFilteredPolicy` |
24
+ | **ScopedCasbinAdapter** | Generic read-only `FilteredAdapter` for the scoped RBAC model — reads one principal's edges + the shared hierarchy from a single `PolicyDefinition` table |
27
25
  | **AbstractAuthRegistry** | Shared base class for authentication strategy registry and authorization enforcer registry |
28
26
 
29
27
  ### Authorization Flow (7 Steps)
@@ -72,6 +70,7 @@ flowchart TD
72
70
  | `Authorization.RULES` | `'authorization.rules'` | Context key for cached rules |
73
71
  | `Authorization.SKIP_AUTHORIZATION` | `'authorization.skip'` | Context key to dynamically skip authorization |
74
72
  | `Authorization.ENFORCER` | `'authorization.enforcer'` | Binding key prefix for enforcers |
73
+ | `Authorization.DOMAIN` | `'authorization.domain'` | Context key for the resolved request domain scope (set by the provider when domain scoping is in play) |
75
74
 
76
75
  ### Authorization Actions
77
76
 
@@ -125,21 +124,72 @@ flowchart TD
125
124
 
126
125
  | Constant | Value | Description |
127
126
  |----------|-------|-------------|
128
- | `CasbinEnforcerCachedDrivers.IN_MEMORY` | `'in-memory'` | In-memory cache with periodic invalidation |
129
- | `CasbinEnforcerCachedDrivers.REDIS` | `'redis'` | Redis-backed cache with TTL |
127
+ | `CasbinEnforcerCachedDrivers.REDIS` | `'redis'` | Redis-backed per-user line cache with TTL (the only cache driver) |
130
128
 
131
129
  `CasbinEnforcerCachedDrivers.SCHEME_SET` contains all valid drivers. `CasbinEnforcerCachedDrivers.isValid(input)` checks membership.
132
130
 
133
- ### Casbin Rule Variants
131
+ > The in-memory cache driver was removed. Caching is **Redis-only**: set `cached` to
132
+ > `{ use: true, driver: 'redis', options: { connection, expiresIn, keyFn } }`, or `{ use: false }`
133
+ > to disable caching (every request rebuilds the user's policy from the datasource).
134
+
135
+ ### Casbin Domain Matching Functions
136
+
137
+ Built-in Casbin matching functions selectable for `ICasbinEnforcerOptions.domainMatching.fn`. Each value maps 1:1 to a Casbin `Util.*Func` export and is applied to the **domain slot** of a role definition (e.g. `g`).
134
138
 
135
139
  | Constant | Value | Description |
136
140
  |----------|-------|-------------|
137
- | `CasbinRuleVariants.POLICY` | `'policy'` | Database variant column value for permission rules |
138
- | `CasbinRuleVariants.GROUP` | `'group'` | Database variant column value for role assignments |
139
- | `CasbinRuleVariants.P` | `'p'` | Casbin line prefix for policy rules |
140
- | `CasbinRuleVariants.G` | `'g'` | Casbin line prefix for grouping rules |
141
+ | `CasbinDomainMatchingFunctions.KEY_MATCH` | `'keyMatch'` | `*` is the only wildcard; exact compare otherwise. Recommended for `Merchant_<uuid>`-style domains |
142
+ | `CasbinDomainMatchingFunctions.KEY_MATCH_2` | `'keyMatch2'` | Adds URL-path `:param` segment matching |
143
+ | `CasbinDomainMatchingFunctions.KEY_MATCH_3` | `'keyMatch3'` | Adds `{param}` segment matching |
144
+ | `CasbinDomainMatchingFunctions.KEY_MATCH_4` | `'keyMatch4'` | `{param}` with repeated-name equality checks |
145
+ | `CasbinDomainMatchingFunctions.REGEX_MATCH` | `'regexMatch'` | Treats the stored/policy value as a full regular expression |
146
+
147
+ `CasbinDomainMatchingFunctions.SCHEME_SET` contains all valid values. `CasbinDomainMatchingFunctions.isValid(input)` checks membership. Companion type: `TCasbinDomainMatchingFunction`.
148
+
149
+ > [!IMPORTANT]
150
+ > The function is applied as `fn(requestDomain, policyDomain)` — the wildcard must live on the **stored/policy** side. With `keyMatch`: `keyMatch("Merchant_X", "*") === true`, `keyMatch("Merchant_X", "Merchant_X") === true`, `keyMatch("Merchant_X", "Merchant_Y") === false`. Store only `*` or exact domain values (never partial patterns like `Merchant_*`) to keep tenant isolation guaranteed.
151
+
152
+ ### Casbin Rule Variants
153
+
154
+ `CasbinRuleVariants` holds **only the Casbin line prefixes** declared by the model, numbered in
155
+ request-tuple order (`sub → dom → obj → act`):
156
+
157
+ | Constant | Value | Relation |
158
+ |----------|-------|----------|
159
+ | `CasbinRuleVariants.P` | `'p'` | Permission policy line |
160
+ | `CasbinRuleVariants.G` | `'g'` | Role membership + role inheritance (the `sub` axis) |
161
+ | `CasbinRuleVariants.G2` | `'g2'` | User→domain membership (the `dom` axis) |
162
+ | `CasbinRuleVariants.G3` | `'g3'` | Domain hierarchy (the `dom` axis) |
163
+ | `CasbinRuleVariants.G4` | `'g4'` | Resource hierarchy (the `obj` axis, via `objectMatch`) |
164
+ | `CasbinRuleVariants.G5` | `'g5'` | Action hierarchy (the `act` axis) |
165
+
166
+ ### Authorization Policy Variants
167
+
168
+ The DB `variant` discriminator (the kind of "edge" stored in `PolicyDefinition`) lives on
169
+ `AuthorizationPolicyVariants`. Each entry carries `action` (the DB value) and `rule` (the Casbin prefix
170
+ the adapter emits for that edge):
171
+
172
+ | Variant | `action` (DB) | `rule` | Meaning |
173
+ |---------|---------------|--------|---------|
174
+ | `GRANT` | `'grant'` | `p` | Give a permission to a User or Role |
175
+ | `ASSIGN_ROLE` | `'assign_role'` | `g` | Give a User a Role (optionally domain-scoped) |
176
+ | `ROLE_INHERITS` | `'role_inherits'` | `g` | Role inherits another Role |
177
+ | `JOIN_DOMAIN` | `'join_domain'` | `g2` | User is a member of a Domain |
178
+ | `DOMAIN_INHERITS` | `'domain_inherits'` | `g3` | Domain nested under a parent Domain |
179
+ | `RESOURCE_INHERITS` | `'resource_inherits'` | `g4` | Resource nested under a broader Resource |
180
+ | `ACTION_INHERITS` | `'action_inherits'` | `g5` | Action implied by a broader Action |
141
181
 
142
- `CasbinRuleVariants.SCHEME_SET` contains `POLICY` and `GROUP` (the DB variants). `CasbinRuleVariants.isValid(input)` checks membership against the DB variants.
182
+ `AuthorizationPolicyVariants.isValidAction(input)` / `isValidRule(input)` check membership;
183
+ `ACTION_SCHEME_SET` / `RULE_SCHEME_SET` hold the sets.
184
+
185
+ ### Authorization Domain Scopes
186
+
187
+ Sentinel domain values used on `grant` rows:
188
+
189
+ | Constant | Value | Meaning |
190
+ |----------|-------|---------|
191
+ | `AuthorizationDomainScopes.ANY_MEMBER` | `'ANY_MEMBER'` | Applies in every domain the subject joined (checked via `g2`) |
192
+ | `AuthorizationDomainScopes.SYSTEM_WIDE` | `'SYSTEM_WIDE'` | Applies system-wide, bypassing membership (super-admin) |
143
193
 
144
194
  > [!NOTE]
145
195
  > All constant classes follow the same pattern: static readonly values + `SCHEME_SET: Set<string>` + `isValid(input): boolean`. Each class also has a companion type alias generated via `TConstValue<typeof ClassName>` (e.g., `TAuthorizationAction`, `TAuthorizationDecision`, `TCasbinRuleVariant`).
@@ -174,22 +224,26 @@ import {
174
224
 
175
225
  // Adapters
176
226
  BaseFilteredAdapter,
177
- DrizzleCasbinAdapter,
227
+ ScopedCasbinAdapter,
228
+
229
+ // Scoped RBAC model
230
+ CASBIN_RBAC_DOMAIN_SCOPED_MODEL,
178
231
 
179
232
  // Models
180
233
  AuthorizationRole,
181
- StringAuthorizationAction,
182
- StringAuthorizationResource,
183
234
 
184
235
  // Constants
185
236
  Authorization,
186
237
  AuthorizationActions,
187
238
  AuthorizationDecisions,
239
+ AuthorizationDomainScopes,
240
+ AuthorizationPolicyVariants,
188
241
  AuthorizationRoles,
189
242
  AuthorizationEnforcerTypes,
190
243
  CasbinEnforcerModelDrivers,
191
244
  CasbinEnforcerCachedDrivers,
192
245
  CasbinRuleVariants,
246
+ CasbinDomainMatchingFunctions,
193
247
 
194
248
  // Binding keys
195
249
  AuthorizeBindingKeys,
@@ -203,19 +257,15 @@ import type {
203
257
  IAuthorizationSpec,
204
258
  IAuthorizationRequest,
205
259
  IAuthorizationRole,
206
- IAuthorizationComparable,
207
260
 
208
261
  // Casbin options
209
262
  ICasbinEnforcerOptions,
210
- ICasbinEnforcerCachedMemory,
211
263
  ICasbinEnforcerCachedRedis,
212
264
 
213
265
  // Adapter types
214
- IBaseFilteredAdapterEntities,
215
266
  ICasbinPolicyFilter,
216
- TBasePolicyRow,
217
- IDrizzleCasbinEntities,
218
- IDrizzleCasbinAdapterOptions,
267
+ IScopedCasbinEntities,
268
+ IScopedCasbinPolicyFilter,
219
269
 
220
270
  // Function & utility types
221
271
  TAuthorizeFn,
@@ -230,6 +280,7 @@ import type {
230
280
  TCasbinEnforcerCachedDriver,
231
281
  TCasbinEnforcerModelDriver,
232
282
  TCasbinRuleVariant,
283
+ TCasbinDomainMatchingFunction,
233
284
  } from '@venizia/ignis';
234
285
  ```
235
286
 
@@ -290,18 +341,20 @@ import {
290
341
  AuthorizationEnforcerTypes,
291
342
  CasbinAuthorizationEnforcer,
292
343
  CasbinEnforcerModelDrivers,
293
- DrizzleCasbinAdapter,
344
+ ScopedCasbinAdapter,
345
+ CASBIN_RBAC_DOMAIN_SCOPED_MODEL,
294
346
  } from '@venizia/ignis';
295
- import path from 'node:path';
296
347
 
297
- // Create adapter (Drizzle example)
298
- const adapter = new DrizzleCasbinAdapter({
348
+ // The generic scoped adapter reads one principal's edges + the shared hierarchy from a single
349
+ // PolicyDefinition edge table. No subclassing; configure it with IScopedCasbinEntities.
350
+ const adapter = new ScopedCasbinAdapter({
299
351
  dataSource,
300
352
  entities: {
301
- permission: { tableName: 'Permission', principalType: 'Permission' },
302
- role: { tableName: 'Role', principalType: 'Role' },
303
- policyDefinition: { tableName: 'PolicyDefinition', principalType: 'PolicyDefinition' },
304
- domain: { principalType: 'Organization' },
353
+ policyDefinition: { tableName: 'PolicyDefinition', schemaName: 'identity' },
354
+ permission: { tableName: 'Permission', schemaName: 'identity' },
355
+ principals: { user: 'User', role: 'Role' }, // casbin name prefixes
356
+ domainTypes: ['Merchant', 'Organizer'], // domain types you scope on
357
+ softDelete: { use: true, columnName: 'deleted_at' },
305
358
  },
306
359
  });
307
360
 
@@ -313,9 +366,10 @@ AuthorizationEnforcerRegistry.getInstance().register({
313
366
  type: AuthorizationEnforcerTypes.CASBIN,
314
367
  options: {
315
368
  model: {
316
- driver: CasbinEnforcerModelDrivers.FILE,
317
- definition: path.resolve(__dirname, './security/rbac_with_domains_deny.conf'),
369
+ driver: CasbinEnforcerModelDrivers.TEXT,
370
+ definition: CASBIN_RBAC_DOMAIN_SCOPED_MODEL,
318
371
  },
372
+ isScoped: true, // 4-token (sub, dom, obj, act); auto-registers keyMatch + objectMatch
319
373
  adapter,
320
374
  cached: {
321
375
  use: true,
@@ -323,15 +377,12 @@ AuthorizationEnforcerRegistry.getInstance().register({
323
377
  options: {
324
378
  connection: redisHelper,
325
379
  expiresIn: 5 * 60 * 1000, // 5 minutes
326
- keyFn: ({ user }) => `authz:policies:${user.userId}`,
380
+ keyFn: ({ user }) => `authz:policies:${user.principalType}:${user.userId}`,
327
381
  },
328
382
  },
329
- normalizePayloadFn: ({ user, action, resource }) => ({
330
- subject: `user_${user.userId}`,
331
- domain: `Organization_${user.organizationId}`,
332
- resource,
333
- action,
334
- }),
383
+ // poolSize / poolAcquireTimeoutMs are optional (defaults 16 / 5000ms).
384
+ // In scoped mode you do NOT pass domainMatching or normalizePayloadFn — the request domain
385
+ // is supplied by the provider's domain resolver (see "Domain scoping" in usage.md).
335
386
  },
336
387
  }],
337
388
  });
@@ -361,7 +412,8 @@ import {
361
412
  AuthorizationEnforcerTypes,
362
413
  CasbinAuthorizationEnforcer,
363
414
  CasbinEnforcerModelDrivers,
364
- DrizzleCasbinAdapter,
415
+ ScopedCasbinAdapter,
416
+ CASBIN_RBAC_DOMAIN_SCOPED_MODEL,
365
417
  BaseApplication,
366
418
  IAuthorizeOptions,
367
419
  } from '@venizia/ignis';
@@ -378,7 +430,7 @@ export class Application extends BaseApplication {
378
430
  this.component(AuthorizeComponent);
379
431
 
380
432
  // Step 3: Register enforcer(s) with co-located options
381
- const adapter = new DrizzleCasbinAdapter({ dataSource, entities: { ... } });
433
+ const adapter = new ScopedCasbinAdapter({ dataSource, entities: { /* IScopedCasbinEntities */ } });
382
434
 
383
435
  AuthorizationEnforcerRegistry.getInstance().register({
384
436
  container: this,
@@ -388,9 +440,10 @@ export class Application extends BaseApplication {
388
440
  type: AuthorizationEnforcerTypes.CASBIN,
389
441
  options: {
390
442
  model: {
391
- driver: CasbinEnforcerModelDrivers.FILE,
392
- definition: path.resolve(__dirname, './security/rbac_model.conf'),
443
+ driver: CasbinEnforcerModelDrivers.TEXT,
444
+ definition: CASBIN_RBAC_DOMAIN_SCOPED_MODEL,
393
445
  },
446
+ isScoped: true,
394
447
  adapter,
395
448
  cached: { use: false },
396
449
  },
@@ -416,11 +469,14 @@ Global authorization settings. Bound to the container before registering `Author
416
469
  |--------|------|---------|-------------|
417
470
  | `defaultDecision` | `TAuthorizationDecision` | -- | **Required.** Decision when enforcer returns `ABSTAIN` (`'allow'`, `'deny'`, or `'abstain'`) |
418
471
  | `alwaysAllowRoles` | `string[]` | `[]` | Roles that bypass all authorization checks (global) |
472
+ | `domainResolver` | `TAuthorizationDomainResolver` | -- | Fallback domain resolver used when a route's `spec.domain` is not set. Returns `{ type, id }` or `null` (→ `SYSTEM_WIDE`) |
419
473
 
420
474
  ```typescript
421
475
  interface IAuthorizeOptions {
422
476
  defaultDecision: TAuthorizationDecision;
423
477
  alwaysAllowRoles?: string[];
478
+ /** Fallback domain resolver used when a route's spec has no `domain`. */
479
+ domainResolver?: TAuthorizationDomainResolver;
424
480
  }
425
481
  ```
426
482
 
@@ -430,10 +486,14 @@ Casbin-specific options, provided per-enforcer via `AuthorizationEnforcerRegistr
430
486
 
431
487
  | Option | Type | Default | Description |
432
488
  |--------|------|---------|-------------|
433
- | `model` | `{ driver, definition }` | -- | **Required.** Casbin model definition (file path or inline text) |
434
- | `cached` | `{ use: false } \| { use: true, driver, options }` | -- | **Required.** Caching configuration |
435
- | `adapter` | `Adapter` | -- | Casbin adapter instance (e.g., `DrizzleCasbinAdapter`) |
436
- | `normalizePayloadFn` | `(opts) => { subject, resource, action, domain? }` | -- | Normalize subject/resource/action before evaluation |
489
+ | `model` | `{ driver, definition }` | -- | **Required.** Casbin model definition (file path or inline text). For scoped RBAC, use `CASBIN_RBAC_DOMAIN_SCOPED_MODEL` |
490
+ | `cached` | `{ use: false } \| { use: true, driver: 'redis', options }` | -- | **Required.** Caching configuration (Redis-only) |
491
+ | `adapter` | `Adapter` | -- | Casbin adapter instance (e.g., `ScopedCasbinAdapter`) |
492
+ | `isScoped` | `boolean` | `false` | Enable the scoped model: 4-token `(sub, dom, obj, act)` requests; auto-registers `keyMatch` on `g` + `objectMatch` on the resource relation |
493
+ | `poolSize` | `number` | `16` | Number of pooled enforcers (each request enforces on its own) |
494
+ | `poolAcquireTimeoutMs` | `number` | `5000` | Max ms to wait for a free pooled enforcer before failing closed |
495
+ | `normalizePayloadFn` | `(opts) => { subject, resource, action, domain? }` | -- | (Non-scoped/custom) normalize subject/resource/action before evaluation |
496
+ | `domainMatching` | `{ roleDefinition: string; fn: TCasbinDomainMatchingFunction }` | -- | (Non-scoped) opt-in domain matching function on a role definition. **Not needed when `isScoped: true`** — the scoped model registers its matchers automatically |
437
497
 
438
498
  ```typescript
439
499
  interface ICasbinEnforcerOptions<
@@ -448,15 +508,17 @@ interface ICasbinEnforcerOptions<
448
508
 
449
509
  cached:
450
510
  | { use: false }
451
- | { use: true; driver: 'in-memory'; options: { expiresIn: number } }
452
- | { use: true; driver: 'redis'; options: {
453
- connection: DefaultRedisHelper;
454
- expiresIn: number;
455
- keyFn: (opts: { user: { principalType: string } & IAuthUser }) => ValueOrPromise<string>;
456
- } };
511
+ | (ICasbinEnforcerCachedRedis & { use: true });
457
512
 
458
513
  adapter?: TAdapter;
459
514
 
515
+ // Enable the scoped RBAC model (4-token requests + auto-registered matchers).
516
+ isScoped?: boolean;
517
+
518
+ // Per-request enforcer pool (concurrency-safe; fail-closed on error).
519
+ poolSize?: number; // default 16
520
+ poolAcquireTimeoutMs?: number; // default 5000
521
+
460
522
  normalizePayloadFn?(opts: {
461
523
  user: IAuthUser;
462
524
  action: TAction;
@@ -468,6 +530,13 @@ interface ICasbinEnforcerOptions<
468
530
  action: string;
469
531
  domain?: string;
470
532
  };
533
+
534
+ // Non-scoped only. Registers a Casbin domain matching function on the named role definition.
535
+ // When isScoped is true, the scoped model registers its own matchers — do not set this.
536
+ domainMatching?: {
537
+ roleDefinition: string; // e.g. 'g'
538
+ fn: TCasbinDomainMatchingFunction;
539
+ };
471
540
  }
472
541
  ```
473
542
 
@@ -476,25 +545,19 @@ interface ICasbinEnforcerOptions<
476
545
 
477
546
  #### Cache Configuration Types
478
547
 
479
- The `cached` field is a discriminated union:
548
+ The `cached` field is a discriminated union. **Caching is Redis-only** (the in-memory driver was removed):
480
549
 
481
550
  ```typescript
482
- // No caching
551
+ // No caching — every request rebuilds the user's policy from the datasource.
483
552
  interface { use: false }
484
553
 
485
- // In-memory cache (CachedEnforcer with periodic invalidation timer)
486
- interface ICasbinEnforcerCachedMemory {
487
- driver: 'in-memory';
488
- options: { expiresIn: number };
489
- }
490
-
491
- // Redis cache (store/retrieve policy lines from Redis)
554
+ // Redis cache (store/retrieve the user's policy lines from Redis, TTL via PX).
492
555
  interface ICasbinEnforcerCachedRedis {
493
556
  driver: 'redis';
494
557
  options: {
495
558
  connection: DefaultRedisHelper;
496
559
  expiresIn: number;
497
- keyFn: (opts: { user: { principalType: string } & IAuthUser }) => ValueOrPromise<string>;
560
+ keyFn: (opts: { user: IAuthorizationUser }) => ValueOrPromise<string>;
498
561
  };
499
562
  }
500
563
  ```
@@ -519,9 +582,6 @@ interface IAuthorizationSpec<E extends Env = Env, TAction = string, TResource =
519
582
  }
520
583
  ```
521
584
 
522
- > [!NOTE]
523
- > `TAction` and `TResource` default to `string` but can accept `IAuthorizationComparable` implementations (e.g., `StringAuthorizationAction`) for custom comparison logic.
524
-
525
585
  ### TAuthorizationConditions
526
586
 
527
587
  Key-value conditions for attribute-based access control. Values are compared with strict equality (`===`).
@@ -598,6 +658,7 @@ declare module 'hono' {
598
658
  // Authorization
599
659
  [Authorization.RULES]: unknown;
600
660
  [Authorization.SKIP_AUTHORIZATION]: boolean;
661
+ [Authorization.DOMAIN]: string;
601
662
  }
602
663
  }
603
664
  ```
@@ -608,6 +669,7 @@ declare module 'hono' {
608
669
  |-----|----------|------|-------------|
609
670
  | `'authorization.rules'` | `Authorization.RULES` | `unknown` | Cached rules built by the enforcer. Type depends on enforcer implementation. |
610
671
  | `'authorization.skip'` | `Authorization.SKIP_AUTHORIZATION` | `boolean` | Set to `true` to dynamically skip authorization for this request. |
672
+ | `'authorization.domain'` | `Authorization.DOMAIN` | `string` | Resolved request domain scope (`"<Type>_<id>"` or `SYSTEM_WIDE`); set by the provider when `spec.domain` or a global `domainResolver` is in play, and read by the enforcer. |
611
673
 
612
674
  ### Authentication Variables Used by Authorization
613
675