@via-profit/ability 3.6.4 → 3.7.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.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
- # @via-profit/ability
1
+ # @via-profit/Ability
2
2
 
3
- > A set of services partially implementing the [Attribute Based Access Control](https://en.wikipedia.org/wiki/Attribute-based_access_control) principle.
4
- > The package allows you to describe rules, combine them into groups, form policies, and apply them to data to determine permissions.
3
+ > A set of services that partially implement the [Attribute Based Access Control](https://en.wikipedia.org/wiki/Attribute-based_access_control) principle.
4
+ > Allows you to describe rules, combine them into groups, form policies, and apply them to data to determine permissions.
5
5
 
6
6
  ![npm version](https://img.shields.io/npm/v/%40via-profit/ability)
7
7
  ![npm downloads](https://img.shields.io/npm/dm/%40via-profit/ability)
@@ -13,1306 +13,386 @@
13
13
 
14
14
  ## Language / Язык
15
15
 
16
- - [🇬🇧 English](/docs/en/README.md)
16
+ [//]: # (- [🇬🇧 English](/docs/en/README.md))
17
17
  - [🇷🇺 Русский](/docs/ru/README.md)
18
18
 
19
19
  ## Purpose
20
20
 
21
- The package is intended as a **lightweight and extremely simple alternative** to heavy access control systems.
22
- No complex configurations, no dependencies – just a minimal set of tools that allows you to describe rules and policies in a very simple DSL.
21
+ The project was designed to cover typical access control scenarios without unnecessary complexity. We needed a lightweight ABAC engine with a simple DSL, automatic TypeScript type generation — and no external dependencies.
23
22
 
24
- Unlike classic ABAC models, where policies work on the principle *"matched → applied, didn't match → ignored"*, Ability uses a **simplified and more predictable state‑machine model**:
25
23
 
26
- - **all** matching policies are executed in the order they are declared,
27
- - each policy can **set** a state (`permit` or `deny`),
28
- - each policy can **reset** the state if its conditions are not met,
29
- - the final result is determined by the **last processed policy**, not just the one that matched.
24
+ - [Key Features](#key-features)
25
+ - [Installation](#installation)
26
+ - [Quick Start](#quick-start)
27
+ - [Core Concepts](#core-concepts)
28
+ - [DSL](./dsl.md)
30
29
 
31
- ## Contents
32
30
 
33
- - [Quick start](#quick-start)
34
- - [Key concepts](#key-concepts)
35
- - [DSL](#dsl)
36
- - [Combining policies](#combining-policies)
37
- - [Policy Environment](#policy-environment)
38
- - [TypeScript type generator](#typescript-type-generator)
39
- - [Debugging policies](#debugging-policies)
40
- - [Troubleshooting](#troubleshooting)
41
- - [Design recommendations](#design-recommendations)
42
- - [Examples](#examples)
43
- - [Performance](#performance)
44
- - [Api-Reference](./api.md)
31
+ ## Key Features
45
32
 
46
- ## Quick start
33
+ 1. Simple and expressive DSL — rules read like natural language.
34
+ 2. Support for grouping rules with `all of:` / `any of:`.
35
+ 3. `except` operator for describing exceptions within a policy.
36
+ 4. 9 built-in strategies — DenyOverrides, PermitOverrides, FirstMatch, Priority, and others. Custom strategies can be added.
37
+ 5. Cross-platform — works in Node.js and browsers.
38
+ 6. TypeScript-first — automatic type generation for resources from policies.
39
+ 7. Zero dependencies — lightweight and no external libraries.
40
+ 8. Built-in explain — decision tree with results of each rule for debugging.
41
+ 9. Serialization — export and import policies to/from JSON.
47
42
 
48
- Install the package, write DSL, call the parser, run the resolver.
49
43
 
50
- ### Installation
44
+ ## Installation
51
45
 
52
46
  ```bash
53
47
  npm install @via-profit/ability
54
48
  ```
55
49
 
56
- ```bash
57
- yarn add @via-profit/ability
58
- ```
59
-
60
- ```bash
61
- pnpm add @via-profit/ability
62
- ```
63
-
64
- ### Example: deny access to `passwordHash` to everyone except the owner
65
-
66
- Suppose we have user data:
67
-
68
- ```ts
69
- const user = {
70
- id: '1',
71
- login: 'user-001',
72
- passwordHash: '...',
73
- };
74
- ```
75
-
76
- We need to deny reading `passwordHash` to everyone except the user themselves.
77
-
78
- #### DSL policy
79
-
80
- In the policy language, this looks like:
81
-
82
- ```
83
- deny permission.user.passwordHash if any:
84
- viewer.id is not equals owner.id
85
- ```
86
-
87
- **Explanation:**
88
-
89
- - `deny` – policy effect (deny access)
90
- - `permission.user.passwordHash` – permission key.
91
- - `if any:` – start of conditions block
92
- - `viewer.id is not equals owner.id` – rule: if the requester's ID does not equal the owner's ID
93
-
94
- If `viewer.id` does not equal `owner.id`, the rule is considered satisfied, and the policy returns `deny` – access denied. If the IDs match (i.e., the user requests their own data), the rule does not trigger, and access is allowed.
95
-
96
- _Note: The permission key is formed as `permission.` + your custom key in **dot notation**, e.g., the key `foo.bar.baz` in DSL would be `permission.foo.bar.baz`_
97
-
98
- #### Code check
99
-
100
- ```ts
101
- import { AbilityDSLParser, AbilityResolver } from '@via-profit/ability';
102
-
103
- const dsl = `
104
- deny permission.user.passwordHash if any:
105
- viewer.id is not equals owner.id
50
+ ## Quick Start
51
+
52
+ ```typescript
53
+ import {
54
+ AbilityResolver,
55
+ DenyOverridesStrategy,
56
+ ability
57
+ } from '@via-profit/ability';
58
+
59
+ const policies = ability`
60
+ @name "Read access is allowed only for user with ID 123, if the document is published and today is Saturday"
61
+ permit permission.document.read if all:
62
+
63
+ @name "User ID is 123"
64
+ document.ownerId equals 123
65
+
66
+ @name "Document is published"
67
+ document.status in ["published", "archived"]
68
+
69
+ @name "Today must be Saturday"
70
+ env.today.dayName is 'Saturday'
106
71
  `;
107
72
 
108
- const policies = new AbilityDSLParser(dsl).parse(); // get policies
109
- const resolver = new AbilityResolver(policies); // create resolver
110
-
111
- resolver.enforce('user.passwordHash', {
112
- viewer: { id: '1' },
113
- owner: { id: '2' },
114
- }); // will throw an error – access denied
115
- ```
116
- In `enforce`, the key is passed without the `permission.` prefix – it is automatically removed by the parser.
117
-
118
- ### Interaction model
119
-
120
- First, you describe "raw" policies (SDL, JSON, or using classes). Then, from the "raw" data, you form ready policies (an array of policies). This is done once and allows you to have a single source of truth. Then you can run permission checks in the necessary parts of your code using the already prepared policies and resolver.
121
-
122
- Policies, groups, and rules can be created using:
123
-
124
- - DSL (Domain-Specific Language)
125
- - Classes (classic approach)
126
- - JSON
127
-
128
- **Creating policies using DSL**
129
-
130
- ```ts
131
- import { AbilityDSLParser } from '@via-profit/ability';
73
+ const resolver = new AbilityResolver(policies, DenyOverridesStrategy);
132
74
 
133
- // Describe policies in Ability-DSL language
134
- const dsl = `
135
- # @name Creating an order is available only to persons over 18 years old
136
- permit permission.order.action.create if all:
137
- all of:
138
- user.age gte 18
139
-
140
- # @name Editing the price is available only to the administrator
141
- permit permission.order.data.price if all:
142
- all of:
143
- user.roles contains 'administrator'
144
- `;
145
-
146
- // Define resource types for TypeScript
147
- // Types can be generated automatically (more on that later), or described manually
148
- // In this example, for simplicity, types are described manually
149
- type Resources = {
150
- ['order.action.create']: {
151
- user: {
152
- age: number;
153
- }
154
- }
155
- ['order.data.price']: {
156
- user: {
157
- roles: string[];
158
- }
159
- }
75
+ const environment = {
76
+ today: {
77
+ dayName: new Date().toLocaleDateString('en-US', { weekday: 'long' }),
78
+ },
160
79
  }
161
80
 
162
- // Use the parser to create policies
163
- // Pass the resource type as a generic
164
- const policies = new AbilityDSLParser<Resources>(dsl).parse(); // AbilityPolicy[]
165
-
166
- // The parser will return an array of policies even
167
- // if only one policy is described in the DSL
168
- console.log(policies); // [AbilityPolicy, AbilityPolicy, ...]
169
-
170
- // export ready policies
171
- export default policies;
172
- ```
173
-
174
- For more details on DSL, see the section (DSL)[#dsl]
175
-
176
- **Creating policies using classes (classic approach)**
177
-
178
- This approach is quite verbose, but gives you full control over policies
81
+ // Check the permission
82
+ const result = resolver.resolve('document.read', {
83
+ document: {
84
+ ownerId: 123,
85
+ status: 'published',
86
+ },
87
+ }, environment);
179
88
 
180
- ```ts
181
- import { AbilityPolicy, AbilityRuleSet, AbilityRule, AbilityCompare, AbilityPolicyEffect } from '@via-profit/ability';
182
-
183
- // Define resource types for TypeScript
184
- // Types can be generated automatically (more on that later), or described manually
185
- // In this example, for simplicity, types are described manually
186
- type Resources = {
187
- ['order.action.create']: {
188
- user: {
189
- age: number;
190
- }
191
- }
192
- ['order.data.price']: {
193
- user: {
194
- roles: string[];
195
- }
196
- }
197
- }
89
+ console.log(result.isAllowed()); // true
198
90
 
199
- const policies = [
200
- // first policy
201
- new AbilityPolicy<Resources>({
202
- id: '1',
203
- name: 'Creating an order is available only to persons over 18 years old',
204
- compareMethod: AbilityCompare.and,
205
- effect: AbilityPolicyEffect.permit,
206
- permission: 'order.action.create',
207
- }).addRuleSet(
208
- AbilityRuleSet.and([
209
- // rule
210
- AbilityRule.moreOrEqual('user.age', 18),
211
- ]),
212
- ),
213
-
214
- // second policy
215
- new AbilityPolicy<Resources>({
216
- id: '2',
217
- name: 'Editing the price is available only to the administrator',
218
- compareMethod: AbilityCompare.and,
219
- effect: AbilityPolicyEffect.permit,
220
- permission: 'order.data.price',
221
- }).addRuleSet(
222
- AbilityRuleSet.and([
223
- // rule
224
- AbilityRule.contains('user.roles', 'administrator'),
225
- ])
226
- ),
227
- ];
228
-
229
- // export ready policies
230
- export default policies;
91
+ // Detailed results
92
+ console.log(result.explain());
231
93
  ```
232
94
 
233
- **Creating policies using JSON**
234
-
235
- JSON allows you to store policies in a file or database, for example, in PostgreSQL, which supports working with JSON data.
236
-
237
- Policy, group, and rule classes have methods to export to JSON, so you can form policies in any way and export them to JSON whenever you need it
238
-
239
- ```ts
240
- import { AbilityJSONParser } from '@via-profit/ability';
241
-
242
- // Define resource types for TypeScript
243
- // Types can be generated automatically (more on that later), or described manually
244
- // In this example, for simplicity, types are described manually
245
- type Resources = {
246
- ['order.action.create']: {
247
- user: {
248
- age: number;
249
- }
250
- }
251
- ['order.data.price']: {
252
- user: {
253
- roles: string[];
254
- }
255
- }
256
- }
257
-
258
- // Parse JSON using AbilityJSONParser
259
- // Pass the resource types as a generic
260
- const policies = AbilityJSONParser.parse<Resources>([
261
- {
262
- id: '1',
263
- name: 'Creating an order is available only to persons over 18 years old',
264
- effect: 'permit',
265
- permission: 'order.action.create',
266
- compareMethod: 'and',
267
- ruleSet: [
268
- {
269
- compareMethod: 'and',
270
- rules: [
271
- {
272
- subject: 'user.age',
273
- resource: 18,
274
- condition: '>',
275
- }
276
- ]
277
- }
278
- ],
279
- },
280
- {
281
- id: '2',
282
- name: 'Editing the price is available only to the administrator',
283
- effect: 'permit',
284
- permission: 'order.data.price',
285
- compareMethod: 'and',
286
- ruleSet: [
287
- {
288
- compareMethod: 'and',
289
- rules: [
290
- {
291
- subject: 'user.roles',
292
- resource: 'administrator',
293
- condition: 'contains',
294
- }
295
- ]
296
- }
297
- ]
298
- }
299
- ]);
300
-
301
- export default policies;
302
- ```
95
+ ## Core Concepts
303
96
 
304
- ---
97
+ | Component | Description |
98
+ |----------------|-----------------------------------------------------------------------------|
99
+ | `AbilityPolicy` | A policy – has an `effect` (permit/deny), a `permission`, and a set of rules. |
100
+ | `AbilityRuleSet`| A group of rules combined with `all` (AND) or `any` (OR) operator. |
101
+ | `AbilityRule` | An elementary rule: `subject` `operator` `resource`. |
102
+ | `AbilityCondition` | Comparison operator: `=`, `<>`, `>`, `contains`, `in`, `length >`, etc. |
103
+ | `AbilityMatch` | Check state: `match`, `mismatch`, `pending`, `except-mismatch`. |
104
+ | `AbilityStrategy` | Algorithm for selecting the final effect from multiple matched policies. |
305
105
 
306
106
  ## DSL
307
107
 
308
- > DSL - Domain-Specific Language
108
+ Ability DSL is a declarative language for describing access policies.
109
+ It allows you to describe rules in a human-readable form and then use them at runtime to make decisions.
309
110
 
310
- Ability DSL is a declarative language for describing access policies.
311
- It allows you to define rules in a human-readable form using simple constructs: *policies*, *groups*, *rules*, and *annotations*.
111
+ Ability supports two ways to create policies from DSL:
312
112
 
313
- ### Policy structure
113
+ 1. **Using DSL literal**
114
+ 2. **Using a regular string + AbilityDSLParser**
314
115
 
315
- A policy consists of the following construct:
116
+ ### Creating Policies via DSL Literal
316
117
 
317
- ```
318
- <effect> <permission> if <all|any>:
319
- <group>...
320
- ```
321
-
322
- Where:
323
-
324
- - **effect** – `permit` or `deny`
325
- - **permission** – a string like `permission.foo.bar`
326
- (the `permission.` prefix is required in DSL but automatically removed by the parser)
327
- - **if all:** – all groups must be true
328
- - **if any:** – at least one group must be true
329
- - a policy can contain one or more rule groups
330
-
331
- **Example**
332
-
333
- ```dsl
334
- permit permission.order.update if any:
335
- all of:
336
- user.roles contains 'admin'
337
- user.token is not null
118
+ ```ts
119
+ import { ability } from '@via-profit/ability';
338
120
 
339
- any of:
340
- user.roles contains 'developer'
341
- user.login is equals 'dev'
121
+ const policies = ability`
122
+ permit permission.document.read if all:
123
+ document.ownerId equals user.id
124
+ document.status in ["published", "archived"]
125
+ `;
342
126
  ```
343
127
 
344
- This policy means:
345
-
346
- The `permission.order.update` permission will be granted if **at least one** of the two groups is satisfied:
347
-
348
- 1. `user.roles` contains `'admin'` **and** `user.token` is not `null`
349
- 2. `user.roles` contains `'developer'` **or** `user.login` equals `'dev'`
128
+ - the string inside `ability``…`` is parsed by AbilityDSLParser
129
+ - returns an array of policies (`AbilityPolicy[]`)
350
130
 
351
- If multiple policies match the key, they are **all executed**, top to bottom.
131
+ ### Creating Policies via Regular String
352
132
 
353
- Each policy:
354
-
355
- - **match**
356
- → sets a new state (`permit` → allow, `deny` → deny)
357
-
358
- - **mismatch**
359
- → **resets the state** to `neutral`
360
- (i.e., cancels the result of the previous policy)
361
-
362
- The final decision is determined by the **last processed policy**, not just the one that matched.
363
-
364
- This means:
365
-
366
- - a policy can **override** the previous one
367
- - a policy can **cancel** the previous one (via mismatch)
368
- - the order of policies in DSL is critically important
369
-
370
- ### Permission key
371
-
372
- Permission keys are written in `dot notation` and support wildcard patterns using the `*` symbol.
373
- This allows grouping permissions and overriding behavior for entire families of operations.
374
-
375
- **How policy matching works**
376
-
377
- If multiple policies match the key, **all policies are executed in order**, top to bottom.
378
- The final decision is determined by the **last state** set during processing.
379
-
380
- This means:
381
-
382
- - a policy can **override** the result of the previous one
383
- - a policy can **cancel** the result of the previous one (via mismatch)
384
- - the order of policies in DSL is critically important
385
-
386
- ### Example of using wildcards
387
-
388
- | Policy (permission) | key | Matches |
389
- |---------------------|------------------------|---------|
390
- | `order.*` | `order.create` | yes |
391
- | `order.*` | `order.update` | yes |
392
- | `order.*` | `user.create` | no |
393
- | `*.create` | `order.create` | yes |
394
- | `*.create` | `user.create` | yes |
395
- | `*.create` | `order.update` | no |
396
- | `user.profile.*` | `user.profile.update` | yes |
397
- | `user.profile.*` | `user.settings.update` | no |
398
-
399
- ### Example policy with wildcard
133
+ If the literal is not available (e.g., in a dynamic environment):
400
134
 
401
135
  ```ts
402
- import { AbilityDSLParser, AbilityResolver } from '@via-profit/ability';
136
+ import { AbilityDSLParser } from '@via-profit/ability';
403
137
 
404
- // DSL is incomplete and shown only for example
405
138
  const dsl = `
406
- permit permission.order.*
407
- deny permission.order.update
139
+ permit permission.document.read if all:
140
+ document.ownerId equals user.id
141
+ document.status in ["published", "archived"]
408
142
  `;
409
143
 
410
144
  const policies = new AbilityDSLParser(dsl).parse();
411
- const resolver = new AbilityResolver(policies);
412
-
413
- resolver.enforce('order.update', resource); // will throw AbilityError
414
145
  ```
415
146
 
416
- ---
417
-
418
- **Explanation**
419
-
420
- The order of policies in DSL determines the final decision.
421
-
422
- Processing goes top to bottom:
423
-
424
- 1. `permit permission.order.*`
425
- - match → state = `allow`
426
-
427
- 2. `deny permission.order.update`
428
- - match → state = `deny`
429
- - the final state overwrites the previous one
430
-
431
- Result:
147
+ Both methods produce the same result.
432
148
 
433
- ```
434
- order.update → deny
435
- order.create → allow
436
- order.delete → allow
437
- order.view → allow
438
- ```
439
-
440
- ### Comments
441
-
442
- Lines starting with the `#` symbol are considered comments and do not affect the result of rules and policies.
443
-
444
- ---
445
-
446
- ### Annotations
447
-
448
- Currently, only one annotation is supported – ’name’, which will be used as the name for the policy, rule group, or rule.
449
-
450
- Annotations are set via comments:
451
-
452
- ```
453
- # @name <name>
454
- ```
455
-
456
- Annotations apply to the **next entity**:
457
-
458
- - policy
459
- - group
460
- - rule
461
-
462
- Example:
463
-
464
- ```dsl
465
- # @name can order update
466
- permit permission.order.update if any:
467
- # @name authorized admin
468
- all of:
469
- # @name contains role admin
470
- user.roles contains 'admin'
471
- ```
472
-
473
- ---
474
-
475
- ### Rule groups
476
-
477
- A group defines how rules are combined within it:
478
-
479
- ```
480
- all of:
481
- <rule>
482
- <rule>
483
-
484
- any of:
485
- <rule>
486
- <rule>
487
- ```
488
-
489
- - `all of:` – logical AND
490
- - `any of:` – logical OR
491
-
492
- `all of` – means the group is considered satisfied if all rules within the group matched.
493
-
494
- `any of` – means the group is considered satisfied if at least one rule within the group matched.
495
-
496
- Each group inside a policy will be evaluated independently of other groups. The final result will be determined by comparing the evaluation results of all groups in the policy.
497
-
498
- Groups can have annotations:
499
-
500
- ```dsl
501
- # @name developer group
502
- any of:
503
- user.roles contains 'developer'
504
- ```
505
-
506
- ---
507
-
508
- ### Rules
509
-
510
- A rule is an atomic condition inside a policy. It defines under what data the policy will be considered matching. Rules are used to set conditions that determine the policy's effect (`permit` or `deny`).
511
-
512
- A rule has the form:
513
-
514
- ```
515
- <subject> <operator> <value?> — value is not specified for all operators (e.g., is null does not require a value).
516
- ```
517
-
518
- #### Subject
519
-
520
- Identifier in dot notation:
521
-
522
- ```
523
- user.roles
524
- env.time.hour
525
- order.total
526
- ```
527
-
528
- #### Operators
529
-
530
- _Synonyms are alternative forms of notation that are also supported by the parser._
531
-
532
- **Basic comparison operators**
533
-
534
- | DSL Operator | Synonyms | Example | Description | Types |
535
- |--------------|----------|--------|----------|------|
536
- | **is equals** | `=`, `==`, `equals` | `age is equals 18` | Strict equality | number, string, boolean |
537
- | **is not equals** | `!=`, `<>`, `not equals` | `role is not equals 'admin'` | Strict inequality | number, string, boolean |
538
- | **greater than** | `>`, `gt` | `age greater than 18` | Greater than | number, date |
539
- | **greater than or equal** | `>=`, `gte` | `age greater than or equal 18` | Greater than or equal | number, date |
540
- | **less than** | `<`, `lt` | `age less than 18` | Less than | number, date |
541
- | **less than or equal** | `<=`, `lte` | `age less than or equal 18` | Less than or equal | number, date |
542
-
543
- **Null operators**
544
-
545
- | DSL Operator | Synonyms | Example | Description | Types |
546
- |--------------|----------|--------|----------|------|
547
- | **is null** | `== null`, `= null` | `middleName is null` | Value is absent | any |
548
- | **is not null** | `!= null` | `middleName is not null` | Value is present | any |
549
-
550
- **Operators for lists (arrays)**
551
-
552
- | DSL Operator | Synonyms | Example | Description | Types |
553
- |--------------|---------------------------|--------|----------|------|
554
- | **in [...]** | - | `role in ['admin', 'manager']` | Value is in the list | number, string |
555
- | **not in [...]** | - | `role not in ['banned']` | Value is not in the list | number, string |
556
- | **contains** | `includes`, `has` | `tags contains 'vip'` | Array contains element | array |
557
- | **not contains** | `not includes`, `not has` | `tags not contains 'vip'` | Array does not contain element | array |
558
-
559
- **Boolean operators**
560
-
561
- | DSL Operator | Synonyms | Example | Description | Types |
562
- |--------------|----------|--------|----------|------|
563
- | **is true** | `= true` | `isActive is true` | Value is true | boolean |
564
- | **is false** | `= false` | `isActive is false` | Value is false | boolean |
565
-
566
- **Length operators**
567
-
568
- | DSL Operator | Synonyms | Example | Description | Types |
569
- |--------------|----------|--------|----------|------|
570
- | **length equals** | `len =` | `tags length equals 3` | Length equals | array, string |
571
- | **length greater than** | `len >` | `tags length greater than 2` | Length greater than | array, string |
572
- | **length less than** | `len <` | `tags length less than 5` | Length less than | array, string |
573
-
574
- **Special operators**
575
-
576
- | DSL Operator | Synonyms | Example | Description | Types |
577
- |--------------|----------|--------|----------|------|
578
- | **always** | — | `always` | Condition always true. Used for global permission or simplifying logic. | special operator |
579
- | **never** | — | `never` | Condition always false. Used for global denial or disabling a rule. | special operator |
580
-
581
- **always**
582
- An operator that always returns `true`.
583
- Used for:
584
-
585
- - global permission (`permit permission.* if all: always`)
586
- - testing
587
- - disabling complex conditions
588
- - creating fallback rules
589
-
590
- **never**
591
- An operator that always returns `false`.
592
- Used for:
593
-
594
- - global denial (`deny permission.* if all: never`)
595
- - temporarily disabling a rule
596
- - explicit negation without conditions
597
-
598
- #### Value
599
-
600
- Supported:
601
-
602
- - strings `'text'`
603
- - numbers `42`
604
- - booleans `true` / `false`
605
- - `null`
606
- - arrays `[1, 2, 3]` / `['foo', false, null, 1, 2, '999']`
607
-
608
- Examples:
609
-
610
- ```dsl
611
- # user age greater than 18
612
- user.age greater than 18
613
-
614
- # array of roles contains role 'admin'
615
- user.roles contains 'admin'
616
-
617
- # order tag is either 'vip' or 'priority'
618
- order.tag in ['vip', 'priority']
619
-
620
- # user token is not null
621
- user.token is not null
622
-
623
- # user login is longer than 12 characters
624
- user.login length greater than 12
625
- ```
626
-
627
- ---
628
-
629
- ### Implicit group
630
-
631
- If rules are written without `all of:` or `any of:`, they are combined by the policy operator:
632
-
633
- ```dsl
634
- permit permission.order.update if all:
635
- user.roles contains 'admin'
636
- user.token is not null
637
- ```
638
-
639
- Equivalent to:
640
-
641
- ```dsl
642
- permit permission.order.update if all:
643
- all of:
644
- user.roles contains 'admin'
645
- user.token is not null
646
- ```
647
-
648
- The implicit group always matches the policy operator (`if all` or `if any`).
649
-
650
- ---
651
-
652
- ### Full example
653
-
654
- ```dsl
655
- # @name order update allowed
656
- permit permission.order.update if any:
657
-
658
- # @name if this is admin
659
- all of:
660
- user.roles contains 'admin'
661
- user.token is not null
662
-
663
- # @name if this is developer
664
- any of:
665
- user.roles contains 'developer'
666
- user.login is equals 'dev'
667
- ```
149
+ ### Typing DSL with Generics
668
150
 
669
- ## Combining policies
670
-
671
- In a real project, you should use several policies at once.
672
-
673
- TODO: using multiple policies
674
-
675
- ## Policy Environment
676
-
677
- **Environment** is an object containing environment data that does not belong to either the user or the resource.
678
- The content of the object is defined by the developer and can be any object composed of primitives.
679
-
680
- - request time,
681
- - IP address,
682
- - device parameters,
683
- - request headers,
684
- - session context,
685
- - any other external conditions.
686
-
687
- The environment is passed to `resolve()` and `enforce()` as the third argument:
151
+ The DSL literal can accept types:
688
152
 
689
153
  ```ts
690
- const environment = {
691
- time: {
692
- hour: new Date().getHours(),
693
- },
694
- ip: req.ip,
695
- }
696
-
697
- resolver.enforce('order.update', resource, environment);
698
- ```
699
-
700
- ### Using environment in rules
701
-
702
- In a policy, you can refer to the environment via the path `env.*`.
703
-
704
- Example policy that denies order updates at night (10 PM – 6 AM):
705
-
706
- ```dsl
707
- # @name Deny updates at night
708
- deny permission.order.update if all:
709
- env.time.hour less than 6
710
- env.time.hour greater or equal than 22
154
+ const policies = ability<Resources, Environment, PolicyTags>`
155
+ permit permission.document.read if all:
156
+ document.ownerId equals user.id
157
+ document.status in ["published", "archived"]
158
+ `;
711
159
  ```
712
160
 
713
- **Retrieving values from environment**
161
+ ### What These Types Provide:
714
162
 
715
- If the rule specifies a path:
163
+ | Type | Description |
164
+ |---------------|--------------------------------------------------------------------|
165
+ | `Resources` | Type of resources available in DSL (`document.*`, `order.*`, etc.) |
166
+ | `Environment` | Type of environment data (`env.time.*`, `env.user.*`) |
167
+ | `PolicyTags` | Types of policy tags (if used) |
716
168
 
717
- - `env.*` value is taken from environment
718
- - `user.*`, `order.*`, `profile.*` → from resource
719
- - literal (`18`, `"admin"`, `true`) → used as is
169
+ ### Generating Types from Policies
720
170
 
721
- Example:
171
+ Ability can automatically generate types based on policies:
722
172
 
723
173
  ```ts
724
- subject: "env.geo.country"
725
- resource: "user.country"
726
- condition: "equal"
727
- ```
728
-
729
- ### Environment in TypeScript
730
-
731
- The Environment type is specified at the `AbilityResolver` level:
732
-
733
- ```ts
734
- const resolver = new AbilityResolver<Resources, Environment>(policies);
735
- ```
736
-
737
- This allows:
174
+ import { AbilityTypeGenerator } from '@via-profit/ability';
738
175
 
739
- - getting autocompletion in the IDE,
740
- - checking the correctness of `env.*` paths,
741
- - avoiding errors when passing the environment.
742
-
743
- > If a rule uses `env.*` but the environment is not passed, the `env.*` value will be `undefined`, and the comparison will be performed as if the environment were not present at all.
744
-
745
- ## TypeScript type generator
746
-
747
- `AbilityTypeGenerator.generateTypeDefs(policies)` generates types for TypeScript based on policies, allowing you not to worry about discrepancies between types and data in policies.
748
-
749
- **Usage example**
750
-
751
- Policies can be stored in DSL or JSON. This example uses a DSL file.
752
-
753
- _policies/policies.dsl_
754
- ```
755
- # @name Update order
756
- permit permission.order.update if all:
757
-
758
- # @name Owner check
759
- all of:
760
- # @name User is owner
761
- user.id = order.ownerId
762
- ```
763
-
764
- _scripts/policies.js_
765
- ```js
766
- const fs = require('node:fs');
767
- const path = require('node:path');
768
- const { AbilityTypeGenerator, AbilityDSLParser } = require('@via-profit/ability');
769
-
770
- // Prepare paths
771
- const dslPath = path.resolve(__dirname, '../src/policies/policies.dsl');
772
- const typeDefsPath = path.join(path.dirname(dslPath), 'policies.types.ts');
773
-
774
- // Read DSL as string
775
- const dsl = fs.readFileSync(dslPath, {encoding: 'utf-8'});
776
-
777
- // Create policies
778
- const policies = new AbilityDSLParser(dsl).parse();
779
-
780
- // Generate TypeScript types
781
176
  const typeDefs = new AbilityTypeGenerator(policies).generateTypeDefs();
782
177
 
783
- // Save TypeScript types to file
784
- fs.writeFileSync(typeDefsPath, typeDefs, {encoding: 'utf-8'});
178
+ fs.writeFileSync('types.gen.ts', typeDefs, { encoding: 'utf-8' });
785
179
  ```
786
180
 
787
- _policies/index.ts_
788
- ```ts
789
- import { AbilityDSLParser, AbilityResolver } from '@via-profit/ability';
790
- import type { Resources } from './policies.types';
791
- import dsl from './policies.dsl';
792
-
793
- const policies = new AbilityDSLParser<Resources>(dsl).parse();
794
-
795
- export const policyResolver = new AbilityResolver(new AbilityDSLParser<Resources>(dsl).parse());
181
+ This creates a file:
796
182
 
797
- export default policyResolver;
798
183
  ```
799
-
800
- **Generated file (example)**
801
-
802
- ```ts
803
- // src/ability/types.generated.ts
804
-
805
- // Automatically generated by via-profit/ability
806
- // Do not edit manually
807
- export type Resources = {
808
- 'order.update': {
809
- readonly user: {
810
- readonly id: string;
811
- };
812
- readonly order: {
813
- readonly ownerId: string;
814
- };
815
- };
816
- };
184
+ types.gen.ts
817
185
  ```
818
186
 
819
- **Usage in code**
187
+ It will contain:
820
188
 
821
189
  ```ts
822
- import { policyResolver } from './policies';
823
-
824
- resolver.enforce('order.update', {
825
- user: { id: 'u1' },
826
- order: { ownerId: 'u1' },
827
- });
190
+ export type Resources = { ... };
191
+ export type Environment = { ... };
192
+ export type PolicyTags = "myTag1" | "myTag2" |
193
+ ...
194
+ ;
828
195
  ```
829
196
 
830
- ## Debugging policies
197
+ ### Using Generated Types
831
198
 
832
- ### Explanations
833
-
834
- To simplify policy debugging, a special class `AbilityResult` is used, which is already included in the final calculation result. `AbilityResult` encapsulates the result of applying all matching policies to the permission key and resource.
835
-
836
- `AbilityResult` contains:
837
-
838
- - list of evaluated policies,
839
- - methods to determine the final effect,
840
- - methods to get explanations in text representation.
841
-
842
- Example:
199
+ After generating types:
843
200
 
844
201
  ```ts
845
- const result = resolver.resolve('order.update', resource);
846
-
847
- if (result.isDenied()) {
848
- console.log('Access denied');
849
- }
850
-
851
- const explanations = result.explain(); // AbilityExplain
852
-
853
- // console.log(explanations.toString());
854
- ```
855
-
856
- ### AbilityExplain
857
-
858
- `AbilityExplain` and related classes (`AbilityExplainPolicy`, `AbilityExplainRuleSet`, `AbilityExplainRule`) allow you to get a human-readable explanation:
859
-
860
- - which policy matched,
861
- - which rule groups matched,
862
- - which rules failed,
863
- - what effect was applied.
864
-
865
- Usage example:
866
-
867
- ```ts
868
- const result = resolver.resolve('order.update', resource);
869
- const explanations = result.explain();
870
-
871
- console.log(explanations.toString());
872
- ```
873
-
874
- Example output:
875
-
876
- ```
877
- ✓ policy «Deny order update for managers» is match
878
- ✓ ruleSet «Managers» is match
879
- ✓ rule «Department managers» is match
880
- ✗ rule «Role manager» is mismatch
881
- ✓ ruleSet «Not administrators» is match
882
- ✓ rule «No role administrator» is match
883
- ```
884
-
885
- ### Output format
886
-
887
- Currently, only one output format is supported – text.
202
+ import { ability, AbilityResolver, DenyOverridesStrategy } from '@via-profit/ability';
203
+ import type { Resources, Environment, PolicyTags } from './types.gen';
888
204
 
889
- The output is structured as: <policy | ruleSet | rule > <name> <is match | is mismatch>
890
-
891
- ## Troubleshooting
892
-
893
- ### Decision model (Default Deny)
894
-
895
- > Why doesn't a `deny` policy turn into `permit` if its conditions are not met?
896
-
897
- Consider a policy that **denies** access to a user aged 16:
898
-
899
- ```ts
900
- const dsl = `
901
- deny permission.test if all:
902
- user.age is equals 16
205
+ const policies = ability<Resources, Environment, PolicyTags>`
206
+ permit permission.document.read if all:
207
+ document.ownerId equals '1'
208
+ document.status in ["published", "archived"]
903
209
  `;
210
+ const resolver = new AbilityResolver(policies, DenyOverridesStrategy);
904
211
 
905
- const policies = new AbilityDSLParser(dsl).parse();
906
- const resolver = new AbilityResolver(policies);
907
-
908
- const result = resolver.resolve('test', {
909
- user: { age: 16 },
212
+ resolver.enforce('document.read', {
213
+ document: {
214
+ ownerId: 1, // ❌ Type number is not assignable to type string
215
+ status: 'published'
216
+ },
910
217
  });
911
218
 
912
- console.log(result.isDenied()); // true ✔
913
- console.log(result.isAllowed()); // false ✔
914
219
  ```
915
220
 
916
- In this case, everything is obvious:
917
- condition satisfied → policy matches → effect `deny` → access denied.
221
+ Now:
918
222
 
919
- **What happens if the conditions are `not satisfied`?**
223
+ - `document.ownerId` and `document.status` are checked for existence and types
224
+ - `document.read` is validated for correctness
225
+ - operators (`equals`, `in`, etc.) are checked for type compatibility
920
226
 
921
- ```ts
922
- const result = resolver.resolve('test', {
923
- user: { age: 12 },
924
- });
227
+ ### Basic Structure
925
228
 
926
- console.log(result.isDenied()); // true ✔
927
- console.log(result.isAllowed()); // false ✔
229
+ ```
230
+ # <comment-line>
231
+ @<annotation> <annotation-value>
232
+ <effect> <permission> if <all|any>:
233
+ <all|any> of: <subject> <operator> <value|resource|env>
234
+ <all|any> of: <subject> <operator> <value|resource|env>
235
+ ...
236
+ except <all|any> of:
237
+ <subject> <operator> <value|resource|env>
238
+ <subject> <operator> <value|resource|env>
239
+ ...
928
240
  ```
929
241
 
930
- At first glance, it might seem that if the condition is not met, the policy should "allow" access.
931
- But that's **not the case**.
932
-
933
- **Decision model: `Default Deny`**
934
-
935
- `AbilityResolver` uses the classic security model:
936
-
937
- > **If there is no matching permit policy → access denied.**
938
-
939
- **What happens in this example:**
940
-
941
- 1. The `deny` policy exists, but its condition is **not satisfied**
942
- → the policy gets `mismatch` status.
943
-
944
- 2. The `deny` policy **does not apply** because the conditions did not match.
945
-
946
- 3. There is no `permit` policy.
947
-
948
- 4. Since there is no granting policy → final decision:
949
- **deny (by default)**.
950
-
951
- **Summary**
952
-
953
- - `deny` with matching conditions → **deny**
954
- - `deny` with non-matching conditions → **deny (default deny)**
955
- - `permit` with matching conditions → **allow**
956
- - `permit` with non-matching conditions → **deny (default deny)**
957
-
958
- **Conclusion**
959
-
960
- **Access is only granted when there is an explicit permit.**
961
-
962
- ## Design recommendations
963
-
964
- ### Naming access keys
965
-
966
- - Use hierarchical keys: `permission.order.create`, `permission.order.update.status`, `permission.user.profile.update`.
967
- - Group by domains: `permission.user.*`, `permission.order.*`, `permission.product.*`.
968
- - Do not mix different domains in one key.
969
-
970
- ### Data structure
971
-
972
- - Explicitly describe `Resources` in TypeScript.
973
- - Do not pass "extra" fields – this complicates understanding.
974
- - Try to keep the data structure for a single `permission` stable.
975
-
976
- ### Designing policies
977
-
978
- - Common rules – via wildcard (`permission.order.*`).
979
- - Specific restrictions – via exact actions (`permission.order.update`).
980
- - Use `effect: deny` for prohibitions.
981
- - Use `effect: permit` for permissions.
982
-
983
- ### Typical mistakes
984
-
985
- - Expecting that the absence of matching policies means deny.
986
- - Mixing business logic and access policies.
987
- - Too large policies with dozens of rules – it's better to split them.
988
-
989
- ### Example of use on the frontend (React)
990
-
991
- **Hook for checking policies**
992
-
993
- ```tsx
994
- // hooks/use-ability.ts
995
- import { useEffect, useState } from 'react';
996
- import { AbilityResolver } from '@via-profit/ability';
997
- import { Resources } from './generated-types';
998
-
999
- export function useAbility<Permission extends keyof Resources>(
1000
- resolver: AbilityResolver<Resources>,
1001
- permission: Permission,
1002
- resource: Resources[Permission],
1003
- ) {
1004
- const [allowed, setAllowed] = useState<boolean | null>(null);
242
+ - `comment-line` - comment
243
+ - `annotation` - annotation (`id`, `name`, `disabled`, `tags`, `priority`)
244
+ - `effect` – `permit` or `deny`
245
+ - `permission` – permission key with `permission.` prefix (e.g., `permission.order.update`)
246
+ - `all` / `any` – logical operator for the rule group
247
+ - `except` - start of the exception block
1005
248
 
1006
- useEffect(() => {
1007
- let cancelled = false;
249
+ ### Rule
1008
250
 
1009
- async function check() {
1010
- try {
1011
- const result = resolver.resolve(permission, resource);
1012
- if (!cancelled) {
1013
- setAllowed(result.isAllowed());
1014
- }
1015
- } catch {
1016
- if (!cancelled) {
1017
- setAllowed(false);
1018
- }
1019
- }
1020
- }
251
+ A rule is the simplest structure that describes what is compared with what and how.
1021
252
 
1022
- check();
253
+ Each rule must start with a resource path (dot-notation), followed by a comparison operator and the value to compare against.
1023
254
 
1024
- return () => {
1025
- cancelled = true;
1026
- };
1027
- }, [resolver, permission, resource]);
255
+ *Rule structure*:
1028
256
 
1029
- return allowed;
1030
- }
1031
257
  ```
1032
-
1033
- **Usage in component**
1034
-
1035
- ```tsx
1036
- function OrderUpdateButton({ order, user }) {
1037
- const allowed = useAbility(resolver, 'order.update', {
1038
- user,
1039
- order,
1040
- });
1041
-
1042
- if (allowed === null) {
1043
- return null; // or loading badge
1044
- }
1045
-
1046
- if (!allowed) {
1047
- return null;
1048
- }
1049
-
1050
- return <button>Update order</button>;
1051
- }
258
+ <subject> <operator> <value|resource|env>
1052
259
  ```
1053
260
 
1054
- ## Examples
1055
-
1056
- ### Example of a complex multi-stage policy
1057
-
1058
- Below is an example of a set of policies for a cinema.
1059
- It demonstrates:
1060
-
1061
- - working with roles (admin, seller, manager, VIP, banned),
1062
- - time restrictions (`env.time.hour`),
1063
- - wildcard permissions (`permission.*`),
1064
- - ticket quantity restrictions,
1065
- - prohibition on selling already sold tickets,
1066
- - combination of `permit`/`deny`,
1067
- - **sequential processing of policies**,
1068
- - **state‑machine model**, where each policy can **set or reset the state**.
1069
-
1070
- ---
1071
-
1072
- **Unlike classic ABAC systems, where `mismatch` is ignored, Ability uses a `state‑machine` model:**
1073
-
1074
- - **match** → policy sets a state (`allow` or `deny`)
1075
- - **mismatch** → policy **resets the state to neutral**
1076
- - the final result is determined by the **last processed policy**
1077
-
1078
- This means:
1079
-
1080
- - a policy can **override** the previous one
1081
- - a policy can **cancel** the previous one (via mismatch)
1082
- - the order of policies in DSL is critically important
1083
- - the final decision does not always match the “intuitive” reading of rules from top to bottom
1084
-
1085
- ### Brief description of the rules
1086
-
1087
- **Administrator**
1088
-
1089
- - Has wildcard rights (`permission.*`)
1090
- - Can edit ticket prices
1091
-
1092
- **Seller**
1093
-
1094
- - Can sell tickets only during working hours (09:00–23:00)
1095
- - Cannot sell tickets if:
1096
- - the cinema is closed,
1097
- - the ticket is already sold
1098
-
1099
- **Manager**
1100
-
1101
- - Has the same rights as the seller
1102
-
1103
- **Buyers**
1104
-
1105
- - A user over 21 years old can buy tickets
1106
- - A VIP user can buy tickets at any time
1107
- - A banned user (`status = banned`) cannot buy tickets
1108
- - Any user cannot buy more than 6 tickets
1109
-
1110
- ### DSL policies
1111
-
1112
- ```dsl
1113
- permit permission.ticket.price.edit if all:
1114
- user.role is equals 'admin'
1115
-
1116
- permit permission.ticket.sell if all:
1117
- user.role is equals 'seller'
1118
- all of:
1119
- env.time.hour greater than or equal 9
1120
- env.time.hour less than or equal 23
1121
-
1122
- permit permission.ticket.buy if all:
1123
- user.age greater than 21
1124
-
1125
- permit permission.ticket.buy if all:
1126
- user.isVIP is true
1127
-
1128
- deny permission.ticket.buy if all:
1129
- user.status is equals 'banned'
1130
-
1131
- deny permission.ticket.sell if all:
1132
- any of:
1133
- env.time.hour less than 9
1134
- env.time.hour greater than 23
1135
-
1136
- permit permission.ticket.sell if all:
1137
- user.role is equals 'manager'
1138
-
1139
- permit permission.* if all:
1140
- user.role is equals 'admin'
1141
-
1142
- deny permission.ticket.buy if all:
1143
- user.ticketsCount greater than or equal 6
1144
-
1145
- deny permission.ticket.sell if all:
1146
- ticket.status is equals 'sold'
1147
261
  ```
262
+ # Simple rule
263
+ user.role equals "admin"
1148
264
 
1149
- **Example: seller sells a ticket at 3:00 PM**
1150
-
1151
- 1. permit seller match → `allow`
1152
- 2. deny closed mismatch → `neutral`
1153
- 3. deny sold mismatch → `neutral`
1154
-
1155
- Result: `neutral → deny`
1156
-
1157
- **Example: VIP buys a ticket at night**
1158
-
1159
- 1. permit age>21 mismatch → `neutral`
1160
- 2. permit VIP match → `allow`
1161
- 3. deny banned mismatch → `neutral`
1162
- 4. deny limit mismatch → `neutral`
1163
-
1164
- Result: `neutral → deny`
265
+ # Comparison with a number
266
+ user.age >= 18
1165
267
 
1166
- **Example: administrator sells a ticket at night**
268
+ # Array membership check
269
+ user.status in ["active", "verified"]
1167
270
 
1168
- 1. permit admin wildcard match → `allow`
1169
- 2. deny closed match → `deny`
1170
- 3. deny sold mismatch → `neutral`
271
+ # Working with array/string length
272
+ user.roles length greater than 2
1171
273
 
1172
- Result: `neutral → deny`
274
+ # Null check
275
+ user.deletedAt is null
1173
276
 
1174
- ### Preparing policies
277
+ # Undefined check
278
+ user.middleName is defined
1175
279
 
1176
- ```ts
1177
- import { AbilityDSLParser } from '@via-profit/ability';
1178
- import cinemaDSL from './policies/cinema.dsl';
1179
-
1180
- export const policies = new AbilityDSLParser(cinemaDSL).parse();
280
+ # Negation
281
+ user.banned not equals true
1181
282
  ```
1182
283
 
1183
- ### Creating a resolver
284
+ ### Groups and Exceptions
1184
285
 
1185
- ```ts
1186
- import { AbilityResolver } from '@via-profit/ability';
1187
- import { policies } from './policies';
286
+ A rule group is a block containing one or more rules.
1188
287
 
1189
- const resolver = new AbilityResolver(policies);
1190
288
  ```
289
+ permit permission.article.edit if all of:
290
+ article.authorId equals user.id
291
+ any of:
292
+ article.status equals "draft"
293
+ user.role equals "editor"
1191
294
 
1192
- ### enforce (throws an error on deny)
1193
-
1194
- ```ts
1195
- resolver.enforce('ticket.buy', {
1196
- user: { age: 25, ticketsCount: 1 },
1197
- env: { time: { hour: 18 } },
1198
- });
295
+ except any of:
296
+ user.banned is true
1199
297
  ```
1200
298
 
1201
- ### resolve (without exceptions)
1202
-
1203
- ```ts
1204
- const result = resolver.resolve('ticket.buy', {
1205
- user: { age: 25, ticketsCount: 1 },
1206
- env: { time: { hour: 18 } },
1207
- });
299
+ ### Annotations
1208
300
 
1209
- if (result.isAllowed()) {
1210
- console.log('Purchase allowed');
1211
- } else {
1212
- console.log('Purchase denied');
1213
- }
1214
301
  ```
1215
-
1216
- **Preparing data for the resolver**
1217
-
1218
- In the examples above, simple constant objects are passed to the resolver:
1219
-
1220
- ```ts
1221
- resolver.enforce('ticket.buy', {
1222
- user: { age: 25 },
1223
- env: { time: { hour: 18 } },
1224
- });
302
+ @name "High priority"
303
+ @description "Description"
304
+ @priority 100
305
+ @disabled true
306
+ deny permission.admin.all if all:
307
+ always
1225
308
  ```
1226
309
 
1227
- This is done for clarity. In a real application, the data for the resolver should be formed dynamically – from the sources available to your server.
310
+ ## Resolution Strategies
1228
311
 
1229
- **User** (`user`) is usually taken from:
312
+ | Strategy | Behavior |
313
+ |-----------------------------|-----------------------------------------------------------------|
314
+ | `DenyOverridesStrategy` | If there is at least one `deny` → `deny`, otherwise `permit` (default) |
315
+ | `PermitOverridesStrategy` | If there is at least one `permit` → `permit`, otherwise `deny` |
316
+ | `FirstMatchStrategy` | Result of the first matching policy |
317
+ | `SequentialLastMatchStrategy` | Result of the last matching policy |
318
+ | `PriorityStrategy` | Selects the policy with the highest `priority` |
319
+ | `AllMustPermitStrategy` | `permit` only if **all** matching policies are `permit` |
320
+ | `OnlyOneApplicableStrategy` | `deny` if more than one policy matches |
321
+ | `AnyPermitStrategy` | `permit` if there is at least one `permit` |
1230
322
 
1231
- - JWT token
1232
- - session
1233
- - database
1234
- - authorization middleware
323
+ Example of using a strategy:
1235
324
 
1236
- Example:
325
+ ```typescript
326
+ import { PriorityStrategy } from '@via-profit/ability';
1237
327
 
1238
- ```ts
1239
- const user = await db.users.findById(session.userId);
328
+ const resolver = new AbilityResolver(policies, PriorityStrategy);
1240
329
  ```
1241
330
 
1242
- **Environment (`env`)**
331
+ ## TypeScript Type Generator
1243
332
 
1244
- These are any external parameters that can affect access:
333
+ Automatically creates a `Resources` type based on all rules in the policies.
1245
334
 
1246
- - current server time
1247
- - time zone
1248
- - IP address
1249
- - request headers
1250
- - system configuration
335
+ ```typescript
336
+ import { AbilityTypeGenerator } from '@via-profit/ability';
1251
337
 
1252
- Example:
338
+ const generator = new AbilityTypeGenerator(policies);
339
+ const typeDefs = generator.generateTypeDefs();
1253
340
 
1254
- ```ts
1255
- const env = {
1256
- time: {
1257
- hour: new Date().getHours(),
1258
- },
1259
- ip: req.ip,
1260
- };
341
+ // Output: export type Resources = {
342
+ // ['document.read']: { readonly ownerId: number; readonly status: string; };
343
+ // ...
344
+ // }
1261
345
  ```
1262
346
 
1263
- **Resource** (e.g., `ticket`)
347
+ The generated types can be used for strict typing of resources when calling `resolver.resolve()`.
1264
348
 
1265
- If the action is related to a specific object – it also needs to be loaded:
349
+ ## API Reference
1266
350
 
1267
- ```ts
1268
- const ticket = await db.tickets.findById(req.params.ticketId);
1269
- ```
351
+ ### `AbilityPolicy`
1270
352
 
1271
- **Context**
353
+ ```typescript
354
+ new AbilityPolicy({ id, name, permission, effect, compareMethod, priority })
355
+ .addRuleSet(ruleSet)
356
+ .check(resource, environment) // -> AbilityMatch
357
+ .explain() // -> AbilityExplain
358
+ ```
1272
359
 
1273
- The context is the object you pass to `resolve` or `enforce`.
1274
- It contains **all the data** that policies may need:
360
+ ### `AbilityRule`
1275
361
 
1276
- - `user` – data about the current user
1277
- - `env` – environment data (time, IP, geography, system settings)
1278
- - `resource` or `ticket` – data about the entity on which the action is performed
1279
- - any other objects you use in the DSL
362
+ ```typescript
363
+ new AbilityRule({ subject, resource, condition })
364
+ // or static factories:
365
+ AbilityRule.equals('user.id', 123)
366
+ AbilityRule.contains('tags', 'admin')
367
+ ```
1280
368
 
1281
- **Important to understand:**
369
+ ### `AbilityResolver`
1282
370
 
1283
- > The context is formed for a specific action and specific policies. You don't need to store it in advance – you collect it dynamically before calling the resolver.
371
+ ```typescript
372
+ const resolver = new AbilityResolver(policies, strategy?);
373
+ resolver.resolve(permission, resource, environment) -> AbilityResult
374
+ resolver.enforce(permission, resource, environment) // throws an error on deny
375
+ ```
1284
376
 
1285
- ## Performance
377
+ ### `AbilityResult`
1286
378
 
1287
- The tests used policies with 10 conditions, nested fields, and environment.
379
+ ```typescript
380
+ result.isAllowed() // boolean
381
+ result.isDenied() // boolean
382
+ result.explain() // AbilityExplain[]
383
+ ```
1288
384
 
1289
- **Tinybench** ([https://github.com/tinylibs/tinybench](https://github.com/tinylibs/tinybench))
385
+ ## Principles
1290
386
 
1291
- | # | Task name | Latency avg (ns) | Latency med (ns) | Throughput avg (ops/s) | Throughput med (ops/s) | Samples |
1292
- |---|-----------------------------------------|------------------------|------------------------|--------------------------|--------------------------|---------|
1293
- | 0 | resolve() — no cache (heavy rules) | 646317 ± 0.32% | 632319 ± 8446.0 | 1555 ± 0.21% | 1581 ± 21 | 3095 |
1294
- | 1 | resolve() — cold cache (heavy rules) | 636363 ± 0.38% | 623092 ± 7885.0 | 1581 ± 0.21% | 1605 ± 20 | 3143 |
1295
- | 2 | resolve() — warm cache (heavy rules) | 631328 ± 0.26% | 621152 ± 6562.5 | 1590 ± 0.17% | 1610 ± 17 | 3168 |
387
+ The package does not perform asynchronous operations. Preparing data for verification is the responsibility of the calling code. This makes the engine's behavior deterministic and easy to test.
1296
388
 
1297
- ```
1298
- Latency (ns)
1299
- 650k | ███████████████████████████████████████ resolve() — no cache
1300
- 640k | █████████████████████████████████████ resolve() — cold cache
1301
- 630k | ████████████████████████████████████ resolve() — warm cache
1302
- --------------------------------------------------------------
1303
- no cache cold cache warm cache
1304
- ```
389
+ ## License
1305
390
 
1306
- ```
1307
- Throughput (ops/s)
1308
- 1600 | ███████████████████████████████████████ resolve() — warm cache
1309
- 1590 | ██████████████████████████████████████ resolve() — cold cache
1310
- 1580 | █████████████████████████████████████ resolve() — no cache
1311
- --------------------------------------------------------------
1312
- no cache cold cache warm cache
1313
- ```
391
+ MIT
1314
392
 
1315
- ## License
393
+ ---
1316
394
 
1317
- This project is licensed under the MIT License. See the [LICENSE](/LICENSE) file for details.
395
+ ## Links
1318
396
 
397
+ - [GitHub repository](https://github.com/via-profit/ability)
398
+ - [DSL](./dsl.md)