@via-profit/ability 3.5.0 → 3.5.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,6 +1,7 @@
1
- # @via-profit/Ability
1
+ ```markdown
2
+ # @via-profit/ability
2
3
 
3
- > A set of services that partially implement the [Attribute Based Access Control](https://en.wikipedia.org/wiki/Attribute-based_access_control) principle.
4
+ > A set of services partially implementing the [Attribute Based Access Control](https://en.wikipedia.org/wiki/Attribute-based_access_control) principle.
4
5
  > The package allows you to describe rules, combine them into groups, form policies, and apply them to data to determine permissions.
5
6
 
6
7
  ![npm version](https://img.shields.io/npm/v/%40via-profit/ability)
@@ -11,7 +12,6 @@
11
12
  ![issues](https://img.shields.io/github/issues/via-profit/ability)
12
13
  ![stars](https://img.shields.io/github/stars/via-profit/ability?style=social)
13
14
 
14
-
15
15
  ## Language / Язык
16
16
 
17
17
  - [🇬🇧 English](/docs/en/README.md)
@@ -19,27 +19,34 @@
19
19
 
20
20
  ## Purpose
21
21
 
22
- The package is intended as a **lightweight and extremely simple alternative** to heavy access control systems.
23
- Without complex configurations, without dependencies just a minimal set of tools that allows you to describe rules and policies in a maximally simple DSL.
22
+ The package is intended as a **lightweight and extremely simple alternative** to heavy access control systems.
23
+ No complex configurations, no dependencies just a minimal set of tools that allows you to describe rules and policies in a very simple DSL.
24
+
25
+ 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**:
26
+
27
+ - **all** matching policies are executed in the order they are declared,
28
+ - each policy can **set** a state (`permit` or `deny`),
29
+ - each policy can **reset** the state if its conditions are not met,
30
+ - the final result is determined by the **last processed policy**, not just the one that matched.
24
31
 
25
- ## Table of Contents
32
+ ## Contents
26
33
 
27
- - [Quick Start](#quick-start)
28
- - [Fundamentals](#fundamentals)
34
+ - [Quick start](#quick-start)
35
+ - [Key concepts](#key-concepts)
29
36
  - [DSL](#dsl)
30
- - [Combining Policies](#combining-policies)
37
+ - [Combining policies](#combining-policies)
31
38
  - [Policy Environment](#policy-environment)
32
- - [TypeScript Type Generator](#typescript-type-generator)
33
- - [Policy Debugging](#policy-debugging)
39
+ - [TypeScript type generator](#typescript-type-generator)
40
+ - [Debugging policies](#debugging-policies)
34
41
  - [Troubleshooting](#troubleshooting)
35
- - [Design Recommendations](#design-recommendations)
42
+ - [Design recommendations](#design-recommendations)
36
43
  - [Examples](#examples)
37
44
  - [Performance](#performance)
38
- - [API Reference](./api.md)
45
+ - [Api-Reference](./api.md)
39
46
 
40
- ## Quick Start
47
+ ## Quick start
41
48
 
42
- Install the package, write DSL, call the parser, and run the resolver.
49
+ Install the package, write DSL, call the parser, run the resolver.
43
50
 
44
51
  ### Installation
45
52
 
@@ -55,7 +62,7 @@ yarn add @via-profit/ability
55
62
  pnpm add @via-profit/ability
56
63
  ```
57
64
 
58
- ### Example: Deny access to `passwordHash` for everyone except the owner
65
+ ### Example: deny access to `passwordHash` to everyone except the owner
59
66
 
60
67
  Suppose we have user data:
61
68
 
@@ -69,7 +76,7 @@ const user = {
69
76
 
70
77
  We need to deny reading `passwordHash` to everyone except the user themselves.
71
78
 
72
- #### DSL Policy
79
+ #### DSL policy
73
80
 
74
81
  In the policy language, this looks like:
75
82
 
@@ -80,16 +87,16 @@ deny permission.user.passwordHash if any:
80
87
 
81
88
  **Explanation:**
82
89
 
83
- - `deny` policy effect (deny access)
84
- - `permission.user.passwordHash` permission key.
85
- - `if any:` start of the condition block
86
- - `viewer.id is not equals owner.id` rule: if the requester's ID is not equal to the owner's ID
90
+ - `deny` policy effect (deny access)
91
+ - `permission.user.passwordHash` permission key.
92
+ - `if any:` start of conditions block
93
+ - `viewer.id is not equals owner.id` rule: if the requester's ID does not equal the owner's ID
87
94
 
88
- If `viewer.id` is not equal to `owner.id`, the rule is 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
+ 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.
89
96
 
90
- *Note: The permission key is formed according to the principle: `permission.` + your custom key in dot notation. For example, the key `foo.bar.baz` in DSL would be `permission.foo.bar.baz`.*
97
+ _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`_
91
98
 
92
- #### Check in Code
99
+ #### Code check
93
100
 
94
101
  ```ts
95
102
  import { AbilityDSLParser, AbilityResolver } from '@via-profit/ability';
@@ -99,60 +106,77 @@ deny permission.user.passwordHash if any:
99
106
  viewer.id is not equals owner.id
100
107
  `;
101
108
 
102
- const policies = new AbilityDSLParser(dsl).parse(); // obtain policies
109
+ const policies = new AbilityDSLParser(dsl).parse(); // get policies
103
110
  const resolver = new AbilityResolver(policies); // create resolver
104
111
 
105
112
  resolver.enforce('user.passwordHash', {
106
113
  viewer: { id: '1' },
107
114
  owner: { id: '2' },
108
- }); // will throw an error access denied
115
+ }); // will throw an error access denied
109
116
  ```
110
- In `enforce`, the key is passed without the `permission.` prefix it is automatically removed by the parser.
117
+ In `enforce`, the key is passed without the `permission.` prefix it is automatically removed by the parser.
118
+
119
+ ## Key concepts
120
+
121
+ Let's list the key points you need to know before starting to use the package:
122
+
123
+ 1. **The resolver (`AbilityResolver`) works on the `Default Deny` principle.**
124
+ If no policy has set a final state, the result will be `deny`
125
+ ([see here](#troubleshooting)).
126
+ To avoid unexpected `deny`, ensure there is at least one `permit` policy that can match.
127
+ Only then add `deny` policies.
128
+
129
+ 2. **Policies are processed sequentially, from top to bottom.**
130
+ Unlike classic ABAC models, where a `mismatch` is simply ignored, Ability uses a **state‑machine model**:
131
+
132
+ - `match` sets the state (`permit` → allow, `deny` → deny),
133
+ - `mismatch` **resets the state to neutral**,
134
+ - the final result is determined by the **last processed policy**, not just the one that matched.
135
+
136
+ 3. **Rules within a policy are also executed sequentially.**
111
137
 
112
- ## Fundamentals
138
+ 4. **In a rule set with the `all` operator, execution stops at the first `mismatch`.**
139
+ In `any` – at the first `match`.
113
140
 
114
- Let’s briefly list the key points you need to know before starting to use the package:
141
+ 5. **Use [DSL](#dsl) to compose policies** it's simpler and more convenient.
115
142
 
116
- 1. The resolver (`AbilityResolver`) follows the **Default Deny** principle. This means that if no policy matches, the result is `deny` ([more details here](#troubleshooting)). To avoid unexpected `deny`, ensure there is at least one `permit` policy that can match. Only then add `deny` policies.
117
- 2. Policies are applied sequentially. If multiple policies match, the result is determined by the last matching policy.
118
- 3. Rules are executed sequentially.
119
- 4. In a rule set (`RuleSet`) with the `all` comparison operator, further rule execution stops as soon as the first rule returns `mismatch`.
120
- 5. Use [DSL](#dsl) to compose policies — it's simpler and more convenient.
121
- 6. For storing policies on the server, use JSON. Policies can be exported to JSON and imported from JSON.
122
- 7. Generally, rely on the principle: if permission is not explicitly granted → access is denied.
123
- 8. Use the built-in cache only if your policies are incredibly complex and contain a large number of rules.
143
+ 6. **Use JSON to store policies on the server.**
144
+ Policies can be exported to JSON and imported back.
124
145
 
125
- ### Interaction Model
146
+ 7. **Follow the principle: if permission is not explicitly granted → access is denied.**
147
+ This is a natural consequence of the `Default Deny` model and state‑machine behavior.
126
148
 
127
- First, you define "raw" policies (using DSL, JSON, or classes). Then, you transform the raw data into ready-to-use policies (an array of policies). This is done once and provides a single source of truth. After that, you can perform permission checks in any part of your code using the prepared policies and the resolver.
149
+ ### Interaction model
128
150
 
129
- Policies, rule sets, and rules can be created using:
151
+ 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.
152
+
153
+ Policies, groups, and rules can be created using:
130
154
 
131
155
  - DSL (Domain-Specific Language)
132
156
  - Classes (classic approach)
133
157
  - JSON
134
158
 
135
- **Creating policies with DSL**
159
+ **Creating policies using DSL**
136
160
 
137
161
  ```ts
138
162
  import { AbilityDSLParser } from '@via-profit/ability';
139
163
 
140
- // Describe policies using Ability-DSL
164
+ // Describe policies in Ability-DSL language
141
165
  const dsl = `
142
- # @name Order creation is only available to persons over 18 years old
166
+ # @name Creating an order is available only to persons over 18 years old
143
167
  permit permission.order.action.create if all:
144
168
  all of:
145
169
  user.age gte 18
146
170
 
147
- # @name Price editing is only available to administrators
171
+ # @name Editing the price is available only to the administrator
148
172
  permit permission.order.data.price if all:
149
173
  all of:
150
174
  user.roles contains 'administrator'
151
175
  `;
152
176
 
153
177
  // Define resource types for TypeScript
154
- // Types can be generated automatically (more on this later) or defined manually
155
- // In this example, for simplicity, types are defined manually
178
+ // Types can be generated automatically (more on that later), or described manually
179
+ // In this example, for simplicity, types are described manually
156
180
  type Resources = {
157
181
  ['order.action.create']: {
158
182
  user: {
@@ -167,29 +191,29 @@ type Resources = {
167
191
  }
168
192
 
169
193
  // Use the parser to create policies
170
- // Pass the resource type as a generic parameter
194
+ // Pass the resource type as a generic
171
195
  const policies = new AbilityDSLParser<Resources>(dsl).parse(); // AbilityPolicy[]
172
196
 
173
- // The parser returns an array of policies even
197
+ // The parser will return an array of policies even
174
198
  // if only one policy is described in the DSL
175
199
  console.log(policies); // [AbilityPolicy, AbilityPolicy, ...]
176
200
 
177
- // Export the ready-to-use policies
201
+ // export ready policies
178
202
  export default policies;
179
203
  ```
180
204
 
181
- For more details about DSL, see the [DSL](#dsl) section.
205
+ For more details on DSL, see the section (DSL)[#dsl]
182
206
 
183
207
  **Creating policies using classes (classic approach)**
184
208
 
185
- This approach is quite verbose but gives you full control over the policies.
209
+ This approach is quite verbose, but gives you full control over policies
186
210
 
187
211
  ```ts
188
212
  import { AbilityPolicy, AbilityRuleSet, AbilityRule, AbilityCompare, AbilityPolicyEffect } from '@via-profit/ability';
189
213
 
190
214
  // Define resource types for TypeScript
191
- // Types can be generated automatically (more on this later) or defined manually
192
- // In this example, for simplicity, types are defined manually
215
+ // Types can be generated automatically (more on that later), or described manually
216
+ // In this example, for simplicity, types are described manually
193
217
  type Resources = {
194
218
  ['order.action.create']: {
195
219
  user: {
@@ -207,7 +231,7 @@ const policies = [
207
231
  // first policy
208
232
  new AbilityPolicy<Resources>({
209
233
  id: '1',
210
- name: 'Order creation is only available to persons over 18 years old',
234
+ name: 'Creating an order is available only to persons over 18 years old',
211
235
  compareMethod: AbilityCompare.and,
212
236
  effect: AbilityPolicyEffect.permit,
213
237
  permission: 'order.action.create',
@@ -221,7 +245,7 @@ const policies = [
221
245
  // second policy
222
246
  new AbilityPolicy<Resources>({
223
247
  id: '2',
224
- name: 'Price editing is only available to administrators',
248
+ name: 'Editing the price is available only to the administrator',
225
249
  compareMethod: AbilityCompare.and,
226
250
  effect: AbilityPolicyEffect.permit,
227
251
  permission: 'order.data.price',
@@ -233,22 +257,22 @@ const policies = [
233
257
  ),
234
258
  ];
235
259
 
236
- // Export the ready-to-use policies
260
+ // export ready policies
237
261
  export default policies;
238
262
  ```
239
263
 
240
- **Creating policies with JSON**
264
+ **Creating policies using JSON**
241
265
 
242
266
  JSON allows you to store policies in a file or database, for example, in PostgreSQL, which supports working with JSON data.
243
267
 
244
- Policy, rule set, and rule classes have JSON export methods, so you can create policies in any way and export them to JSON whenever needed.
268
+ 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
245
269
 
246
270
  ```ts
247
271
  import { AbilityJSONParser } from '@via-profit/ability';
248
272
 
249
273
  // Define resource types for TypeScript
250
- // Types can be generated automatically (more on this later) or defined manually
251
- // In this example, for simplicity, types are defined manually
274
+ // Types can be generated automatically (more on that later), or described manually
275
+ // In this example, for simplicity, types are described manually
252
276
  type Resources = {
253
277
  ['order.action.create']: {
254
278
  user: {
@@ -263,11 +287,11 @@ type Resources = {
263
287
  }
264
288
 
265
289
  // Parse JSON using AbilityJSONParser
266
- // Pass the resource types as a generic parameter
290
+ // Pass the resource types as a generic
267
291
  const policies = AbilityJSONParser.parse<Resources>([
268
292
  {
269
293
  id: '1',
270
- name: 'Order creation is only available to persons over 18 years old',
294
+ name: 'Creating an order is available only to persons over 18 years old',
271
295
  effect: 'permit',
272
296
  permission: 'order.action.create',
273
297
  compareMethod: 'and',
@@ -286,7 +310,7 @@ const policies = AbilityJSONParser.parse<Resources>([
286
310
  },
287
311
  {
288
312
  id: '2',
289
- name: 'Price editing is only available to administrators',
313
+ name: 'Editing the price is available only to the administrator',
290
314
  effect: 'permit',
291
315
  permission: 'order.data.price',
292
316
  compareMethod: 'and',
@@ -307,18 +331,19 @@ const policies = AbilityJSONParser.parse<Resources>([
307
331
 
308
332
  export default policies;
309
333
  ```
334
+
310
335
  ---
311
336
 
312
337
  ## DSL
313
338
 
314
339
  > DSL - Domain-Specific Language
315
340
 
316
- Ability DSL is a declarative language for describing access policies.
341
+ Ability DSL is a declarative language for describing access policies.
317
342
  It allows you to define rules in a human-readable form using simple constructs: *policies*, *groups*, *rules*, and *annotations*.
318
343
 
319
- ### Policy Structure
344
+ ### Policy structure
320
345
 
321
- A policy consists of:
346
+ A policy consists of the following construct:
322
347
 
323
348
  ```
324
349
  <effect> <permission> if <all|any>:
@@ -327,14 +352,14 @@ A policy consists of:
327
352
 
328
353
  Where:
329
354
 
330
- - **effect** `permit` or `deny`
331
- - **permission** a string of the form `permission.foo.bar`, where the `permission.` prefix is mandatory.
332
- - **if all:** all groups must be true
333
- - **if any:** at least one group must be true
355
+ - **effect** `permit` or `deny`
356
+ - **permission** a string like `permission.foo.bar`
357
+ (the `permission.` prefix is required in DSL but automatically removed by the parser)
358
+ - **if all:** all groups must be true
359
+ - **if any:** – at least one group must be true
360
+ - a policy can contain one or more rule groups
334
361
 
335
- A policy can contain one or more rule groups.
336
-
337
- Example:
362
+ **Example**
338
363
 
339
364
  ```dsl
340
365
  permit permission.order.update if any:
@@ -347,36 +372,67 @@ permit permission.order.update if any:
347
372
  user.login is equals 'dev'
348
373
  ```
349
374
 
350
- > The `permission.` prefix is mandatory in DSL but is automatically removed by the parser. Internally, the permission is stored as `order.update`.
375
+ This policy means:
376
+
377
+ The `permission.order.update` permission will be granted if **at least one** of the two groups is satisfied:
378
+
379
+ 1. `user.roles` contains `'admin'` **and** `user.token` is not `null`
380
+ 2. `user.roles` contains `'developer'` **or** `user.login` equals `'dev'`
381
+
382
+ If multiple policies match the key, they are **all executed**, top to bottom.
383
+
384
+ Each policy:
385
+
386
+ - **match**
387
+ → sets a new state (`permit` → allow, `deny` → deny)
351
388
 
352
- The example policy above says: permission `order.update` will be allowed if one of two conditions is met:
353
- 1. `user.roles` contains 'admin' **and** `user.token` is not null
354
- 2. `user.roles` contains 'developer' **or** `user.login` equals 'dev'
389
+ - **mismatch**
390
+ **resets the state** to `neutral`
391
+ (i.e., cancels the result of the previous policy)
355
392
 
356
- ### Permission Key
393
+ The final decision is determined by the **last processed policy**, not just the one that matched.
357
394
 
358
- Permission keys are written in dot notation but support the use of wildcard patterns with the `*` character. This allows grouping of keys and overriding policies with similar keys.
395
+ This means:
359
396
 
360
- If multiple policies match a key, **all of them are executed**. The final result is determined by the **last matching policy**:
397
+ - a policy can **override** the previous one
398
+ - a policy can **cancel** the previous one (via mismatch)
399
+ - the order of policies in DSL is critically important
361
400
 
362
- **Example of using wildcards**
401
+ ### Permission key
363
402
 
364
- | Policy (permission) | Key | Matches |
365
- |---------------------|-----------------------|---------|
366
- | `order.*` | `order.create` | yes |
367
- | `order.*` | `order.update` | yes |
368
- | `order.*` | `user.create` | no |
369
- | `*.create` | `order.create` | yes |
370
- | `*.create` | `user.create` | yes |
371
- | `*.create` | `order.update` | no |
372
- | `user.profile.*` | `user.profile.update` | yes |
373
- | `user.profile.*` | `user.settings.update`| no |
403
+ Permission keys are written in `dot notation` and support wildcard patterns using the `*` symbol.
404
+ This allows grouping permissions and overriding behavior for entire families of operations.
405
+
406
+ **How policy matching works**
407
+
408
+ If multiple policies match the key, **all policies are executed in order**, top to bottom.
409
+ The final decision is determined by the **last state** set during processing.
410
+
411
+ This means:
412
+
413
+ - a policy can **override** the result of the previous one
414
+ - a policy can **cancel** the result of the previous one (via mismatch)
415
+ - the order of policies in DSL is critically important
416
+
417
+ ### Example of using wildcards
418
+
419
+ | Policy (permission) | key | Matches |
420
+ |---------------------|------------------------|---------|
421
+ | `order.*` | `order.create` | yes |
422
+ | `order.*` | `order.update` | yes |
423
+ | `order.*` | `user.create` | no |
424
+ | `*.create` | `order.create` | yes |
425
+ | `*.create` | `user.create` | yes |
426
+ | `*.create` | `order.update` | no |
427
+ | `user.profile.*` | `user.profile.update` | yes |
428
+ | `user.profile.*` | `user.settings.update` | no |
429
+
430
+ ### Example policy with wildcard
374
431
 
375
- **Example of a policy with wildcard**
376
432
  ```ts
377
433
  import { AbilityDSLParser, AbilityResolver } from '@via-profit/ability';
378
434
 
379
- // DSL is not complete, shown for illustration only
435
+ // DSL is incomplete and shown only for example
380
436
  const dsl = `
381
437
  permit permission.order.*
382
438
  deny permission.order.update
@@ -388,42 +444,47 @@ const resolver = new AbilityResolver(policies);
388
444
  resolver.enforce('order.update', resource); // will throw AbilityError
389
445
  ```
390
446
 
447
+ ---
448
+
391
449
  **Explanation**
392
450
 
393
- In DSL, the order of policies matters:
394
- the last matching policy wins.
451
+ The order of policies in DSL determines the final decision.
395
452
 
396
- Therefore:
453
+ Processing goes top to bottom:
397
454
 
398
- 1. `permit` `permission.order.*` allows everything that starts with `order.`
399
- 2. `deny` `permission.order.update` overrides this permission.
455
+ 1. `permit permission.order.*`
456
+ - match state = `allow`
400
457
 
401
- Execution result:
458
+ 2. `deny permission.order.update`
459
+ - match → state = `deny`
460
+ - the final state overwrites the previous one
461
+
462
+ Result:
402
463
 
403
464
  ```
404
465
  order.update → deny
405
- order.create → permit
406
- order.delete → permit
407
- order.view → permit
466
+ order.create → allow
467
+ order.delete → allow
468
+ order.view → allow
408
469
  ```
409
470
 
410
471
  ### Comments
411
472
 
412
- Lines starting with the `#` symbol are considered comments and do not affect the evaluation of rules and policies.
473
+ Lines starting with the `#` symbol are considered comments and do not affect the result of rules and policies.
413
474
 
414
475
  ---
415
476
 
416
477
  ### Annotations
417
478
 
418
- Currently, only one annotation is supported: `name`, which will be used as the name for a policy, rule group, or rule.
479
+ Currently, only one annotation is supported – ’name’, which will be used as the name for the policy, rule group, or rule.
419
480
 
420
- Annotations are specified via comments:
481
+ Annotations are set via comments:
421
482
 
422
483
  ```
423
484
  # @name <name>
424
485
  ```
425
486
 
426
- Annotations apply to the **following entity**:
487
+ Annotations apply to the **next entity**:
427
488
 
428
489
  - policy
429
490
  - group
@@ -442,9 +503,9 @@ permit permission.order.update if any:
442
503
 
443
504
  ---
444
505
 
445
- ### Rule Groups
506
+ ### Rule groups
446
507
 
447
- A group defines how the rules within it are combined:
508
+ A group defines how rules are combined within it:
448
509
 
449
510
  ```
450
511
  all of:
@@ -456,14 +517,14 @@ any of:
456
517
  <rule>
457
518
  ```
458
519
 
459
- - `all of:` logical AND
460
- - `any of:` logical OR
520
+ - `all of:` logical AND
521
+ - `any of:` logical OR
461
522
 
462
- `all of` means that the group is considered satisfied if all rules within the group match.
523
+ `all of` means the group is considered satisfied if all rules within the group matched.
463
524
 
464
- `any of` means that the group is considered satisfied if at least one rule within the group matches.
525
+ `any of` means the group is considered satisfied if at least one rule within the group matched.
465
526
 
466
- Each group within a policy will be evaluated independently of other groups. The final result is determined by comparing the results of all groups in the policy.
527
+ 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.
467
528
 
468
529
  Groups can have annotations:
469
530
 
@@ -477,12 +538,12 @@ any of:
477
538
 
478
539
  ### Rules
479
540
 
480
- A rule is an atomic condition inside a policy. It defines under what data the policy is considered matched. Rules set the conditions that determine the effectiveness of a policy (`permit` or `deny`).
541
+ 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`).
481
542
 
482
543
  A rule has the form:
483
544
 
484
545
  ```
485
- <subject> <operator> <value?> — the value is not required for some operators (e.g., `is null` does not require a value).
546
+ <subject> <operator> <value?> — value is not specified for all operators (e.g., is null does not require a value).
486
547
  ```
487
548
 
488
549
  #### Subject
@@ -497,12 +558,12 @@ order.total
497
558
 
498
559
  #### Operators
499
560
 
500
- *Synonyms are alternative forms of writing that are also supported by the parser.*
561
+ _Synonyms are alternative forms of notation that are also supported by the parser._
501
562
 
502
- **Basic Comparison Operators**
563
+ **Basic comparison operators**
503
564
 
504
565
  | DSL Operator | Synonyms | Example | Description | Types |
505
- |--------------|----------|---------|-------------|-------|
566
+ |--------------|----------|--------|----------|------|
506
567
  | **is equals** | `=`, `==`, `equals` | `age is equals 18` | Strict equality | number, string, boolean |
507
568
  | **is not equals** | `!=`, `<>`, `not equals` | `role is not equals 'admin'` | Strict inequality | number, string, boolean |
508
569
  | **greater than** | `>`, `gt` | `age greater than 18` | Greater than | number, date |
@@ -510,68 +571,64 @@ order.total
510
571
  | **less than** | `<`, `lt` | `age less than 18` | Less than | number, date |
511
572
  | **less than or equal** | `<=`, `lte` | `age less than or equal 18` | Less than or equal | number, date |
512
573
 
513
- **Null Operators**
574
+ **Null operators**
514
575
 
515
576
  | DSL Operator | Synonyms | Example | Description | Types |
516
- |--------------|----------|---------|-------------|-------|
577
+ |--------------|----------|--------|----------|------|
517
578
  | **is null** | `== null`, `= null` | `middleName is null` | Value is absent | any |
518
579
  | **is not null** | `!= null` | `middleName is not null` | Value is present | any |
519
580
 
520
- **Operators for Lists (Arrays)**
581
+ **Operators for lists (arrays)**
521
582
 
522
583
  | DSL Operator | Synonyms | Example | Description | Types |
523
- |--------------|---------------------------|---------|-------------|-------|
584
+ |--------------|---------------------------|--------|----------|------|
524
585
  | **in [...]** | - | `role in ['admin', 'manager']` | Value is in the list | number, string |
525
586
  | **not in [...]** | - | `role not in ['banned']` | Value is not in the list | number, string |
526
- | **contains** | `includes`, `has` | `tags contains 'vip'` | Array contains the element | array |
527
- | **not contains** | `not includes`, `not has` | `tags not contains 'vip'` | Array does not contain the element | array |
587
+ | **contains** | `includes`, `has` | `tags contains 'vip'` | Array contains element | array |
588
+ | **not contains** | `not includes`, `not has` | `tags not contains 'vip'` | Array does not contain element | array |
528
589
 
529
- **Boolean Operators**
590
+ **Boolean operators**
530
591
 
531
592
  | DSL Operator | Synonyms | Example | Description | Types |
532
- |--------------|----------|---------|-------------|-------|
593
+ |--------------|----------|--------|----------|------|
533
594
  | **is true** | `= true` | `isActive is true` | Value is true | boolean |
534
595
  | **is false** | `= false` | `isActive is false` | Value is false | boolean |
535
596
 
536
- **Length Operators**
597
+ **Length operators**
537
598
 
538
599
  | DSL Operator | Synonyms | Example | Description | Types |
539
- |--------------|----------|---------|-------------|-------|
600
+ |--------------|----------|--------|----------|------|
540
601
  | **length equals** | `len =` | `tags length equals 3` | Length equals | array, string |
541
602
  | **length greater than** | `len >` | `tags length greater than 2` | Length greater than | array, string |
542
603
  | **length less than** | `len <` | `tags length less than 5` | Length less than | array, string |
543
604
 
544
- Here is the English version, keeping the structure and tone consistent with your documentation style.
545
-
546
- **Special Operators**
605
+ **Special operators**
547
606
 
548
607
  | DSL Operator | Synonyms | Example | Description | Types |
549
- |--------------|----------|---------|-------------|--------|
550
- | **always** | — | `always` | The condition is always true. Used for global allow rules or simplifying logic. | special operator |
551
- | **never** | — | `never` | The condition is always false. Used for global deny rules or disabling a rule. | special operator |
552
-
608
+ |--------------|----------|--------|----------|------|
609
+ | **always** | — | `always` | Condition always true. Used for global permission or simplifying logic. | special operator |
610
+ | **never** | — | `never` | Condition always false. Used for global denial or disabling a rule. | special operator |
553
611
 
554
612
  **always**
555
- An operator that always returns `true`.
613
+ An operator that always returns `true`.
556
614
  Used for:
557
615
 
558
- - global allow (`permit permission.* if all: always`)
616
+ - global permission (`permit permission.* if all: always`)
559
617
  - testing
560
618
  - disabling complex conditions
561
619
  - creating fallback rules
562
620
 
563
621
  **never**
564
- An operator that always returns `false`.
622
+ An operator that always returns `false`.
565
623
  Used for:
566
624
 
567
- - global deny (`deny permission.* if all: never`)
625
+ - global denial (`deny permission.* if all: never`)
568
626
  - temporarily disabling a rule
569
- - explicit unconditional rejection
570
-
627
+ - explicit negation without conditions
571
628
 
572
629
  #### Value
573
630
 
574
- Supported values:
631
+ Supported:
575
632
 
576
633
  - strings `'text'`
577
634
  - numbers `42`
@@ -585,7 +642,7 @@ Examples:
585
642
  # user age greater than 18
586
643
  user.age greater than 18
587
644
 
588
- # array of roles contains the role 'admin'
645
+ # array of roles contains role 'admin'
589
646
  user.roles contains 'admin'
590
647
 
591
648
  # order tag is either 'vip' or 'priority'
@@ -600,9 +657,9 @@ user.login length greater than 12
600
657
 
601
658
  ---
602
659
 
603
- ### Implicit Group
660
+ ### Implicit group
604
661
 
605
- If rules are written without `all of:` or `any of:`, they are combined using the policy operator:
662
+ If rules are written without `all of:` or `any of:`, they are combined by the policy operator:
606
663
 
607
664
  ```dsl
608
665
  permit permission.order.update if all:
@@ -623,33 +680,33 @@ The implicit group always matches the policy operator (`if all` or `if any`).
623
680
 
624
681
  ---
625
682
 
626
- ### Complete Example
683
+ ### Full example
627
684
 
628
685
  ```dsl
629
686
  # @name order update allowed
630
687
  permit permission.order.update if any:
631
688
 
632
- # @name if admin
689
+ # @name if this is admin
633
690
  all of:
634
691
  user.roles contains 'admin'
635
692
  user.token is not null
636
693
 
637
- # @name if developer
694
+ # @name if this is developer
638
695
  any of:
639
696
  user.roles contains 'developer'
640
697
  user.login is equals 'dev'
641
698
  ```
642
699
 
643
- ## Combining Policies
700
+ ## Combining policies
644
701
 
645
- In a real project, you should use multiple policies at once.
702
+ In a real project, you should use several policies at once.
646
703
 
647
704
  TODO: using multiple policies
648
705
 
649
706
  ## Policy Environment
650
707
 
651
- **Environment** is an object containing context data that does not belong to either the user or the resource.
652
- The content of the object is defined by the developer and can be any object consisting of primitives.
708
+ **Environment** is an object containing environment data that does not belong to either the user or the resource.
709
+ The content of the object is defined by the developer and can be any object composed of primitives.
653
710
 
654
711
  - request time,
655
712
  - IP address,
@@ -658,8 +715,7 @@ The content of the object is defined by the developer and can be any object cons
658
715
  - session context,
659
716
  - any other external conditions.
660
717
 
661
-
662
- Environment is passed to `resolve()` and `enforce()` as the third argument:
718
+ The environment is passed to `resolve()` and `enforce()` as the third argument:
663
719
 
664
720
  ```ts
665
721
  const environment = {
@@ -674,7 +730,7 @@ resolver.enforce('order.update', resource, environment);
674
730
 
675
731
  ### Using environment in rules
676
732
 
677
- In a policy, you can refer to environment via the `env.*` path.
733
+ In a policy, you can refer to the environment via the path `env.*`.
678
734
 
679
735
  Example policy that denies order updates at night (10 PM – 6 AM):
680
736
 
@@ -687,7 +743,7 @@ deny permission.order.update if all:
687
743
 
688
744
  **Retrieving values from environment**
689
745
 
690
- If a path is specified in a rule:
746
+ If the rule specifies a path:
691
747
 
692
748
  - `env.*` → value is taken from environment
693
749
  - `user.*`, `order.*`, `profile.*` → from resource
@@ -703,7 +759,7 @@ condition: "equal"
703
759
 
704
760
  ### Environment in TypeScript
705
761
 
706
- The Environment type is set at the `AbilityResolver` level:
762
+ The Environment type is specified at the `AbilityResolver` level:
707
763
 
708
764
  ```ts
709
765
  const resolver = new AbilityResolver<Resources, Environment>(policies);
@@ -711,17 +767,17 @@ const resolver = new AbilityResolver<Resources, Environment>(policies);
711
767
 
712
768
  This allows:
713
769
 
714
- - getting autocompletion in IDE,
770
+ - getting autocompletion in the IDE,
715
771
  - checking the correctness of `env.*` paths,
716
- - avoiding errors when passing environment.
772
+ - avoiding errors when passing the environment.
717
773
 
718
- > If a rule uses `env.*` but environment is not passed, then the value of `env.*` will be `undefined`, and the comparison will be performed as if the environment were absent.
774
+ > 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.
719
775
 
720
- ## TypeScript Type Generator
776
+ ## TypeScript type generator
721
777
 
722
- `AbilityTypeGenerator.generateTypeDefs(policies)` generates TypeScript types based on policies, allowing you to avoid inconsistencies between types and the data in the policies.
778
+ `AbilityTypeGenerator.generateTypeDefs(policies)` generates types for TypeScript based on policies, allowing you not to worry about discrepancies between types and data in policies.
723
779
 
724
- **Example usage**
780
+ **Usage example**
725
781
 
726
782
  Policies can be stored in DSL or JSON. This example uses a DSL file.
727
783
 
@@ -746,7 +802,7 @@ const { AbilityTypeGenerator, AbilityDSLParser } = require('@via-profit/ability'
746
802
  const dslPath = path.resolve(__dirname, '../src/policies/policies.dsl');
747
803
  const typeDefsPath = path.join(path.dirname(dslPath), 'policies.types.ts');
748
804
 
749
- // Read DSL as a string
805
+ // Read DSL as string
750
806
  const dsl = fs.readFileSync(dslPath, {encoding: 'utf-8'});
751
807
 
752
808
  // Create policies
@@ -770,7 +826,6 @@ const policies = new AbilityDSLParser<Resources>(dsl).parse();
770
826
  export const policyResolver = new AbilityResolver(new AbilityDSLParser<Resources>(dsl).parse());
771
827
 
772
828
  export default policyResolver;
773
-
774
829
  ```
775
830
 
776
831
  **Generated file (example)**
@@ -803,17 +858,17 @@ resolver.enforce('order.update', {
803
858
  });
804
859
  ```
805
860
 
806
- ## Policy Debugging
861
+ ## Debugging policies
807
862
 
808
863
  ### Explanations
809
864
 
810
- To simplify policy debugging, a special `AbilityResult` class is used, which is already included in the final evaluation result. `AbilityResult` encapsulates the outcome of applying all matching policies to a permission key and resource.
865
+ 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.
811
866
 
812
867
  `AbilityResult` contains:
813
868
 
814
- - a list of evaluated policies,
869
+ - list of evaluated policies,
815
870
  - methods to determine the final effect,
816
- - methods to get explanations in textual representation.
871
+ - methods to get explanations in text representation.
817
872
 
818
873
  Example:
819
874
 
@@ -835,8 +890,8 @@ const explanations = result.explain(); // AbilityExplain
835
890
 
836
891
  - which policy matched,
837
892
  - which rule groups matched,
838
- - which rules did not pass,
839
- - which effect was applied.
893
+ - which rules failed,
894
+ - what effect was applied.
840
895
 
841
896
  Usage example:
842
897
 
@@ -858,17 +913,17 @@ Example output:
858
913
  ✓ rule «No role administrator» is match
859
914
  ```
860
915
 
861
- ### Output Format
916
+ ### Output format
862
917
 
863
- Currently, only one output format is supported textual.
918
+ Currently, only one output format is supported text.
864
919
 
865
- The output follows the principle: `<policy | ruleSet | rule> <name> <is match | is mismatch>`
920
+ The output is structured as: <policy | ruleSet | rule > <name> <is match | is mismatch>
866
921
 
867
922
  ## Troubleshooting
868
923
 
869
- ### Decision‑Making Model (Default Deny)
924
+ ### Decision model (Default Deny)
870
925
 
871
- > Why does a `deny` policy not turn into `permit` if its conditions are not met?
926
+ > Why doesn't a `deny` policy turn into `permit` if its conditions are not met?
872
927
 
873
928
  Consider a policy that **denies** access to a user aged 16:
874
929
 
@@ -889,10 +944,10 @@ console.log(result.isDenied()); // true ✔
889
944
  console.log(result.isAllowed()); // false ✔
890
945
  ```
891
946
 
892
- In this case, everything is obvious:
893
- the condition is met the policy matches → effect `deny` → access denied.
947
+ In this case, everything is obvious:
948
+ condition satisfied → policy matches → effect `deny` → access denied.
894
949
 
895
- **What happens if the conditions are *not met*?**
950
+ **What happens if the conditions are `not satisfied`?**
896
951
 
897
952
  ```ts
898
953
  const result = resolver.resolve('test', {
@@ -903,66 +958,66 @@ console.log(result.isDenied()); // true ✔
903
958
  console.log(result.isAllowed()); // false ✔
904
959
  ```
905
960
 
906
- At first glance, it might seem that if the condition is not met, the policy should allow access.
907
- But that is **not the case**.
961
+ At first glance, it might seem that if the condition is not met, the policy should "allow" access.
962
+ But that's **not the case**.
908
963
 
909
- **Decision‑Making Model: `Default Deny`**
964
+ **Decision model: `Default Deny`**
910
965
 
911
966
  `AbilityResolver` uses the classic security model:
912
967
 
913
- > **If there is no matching permitpolicy → access is denied.**
968
+ > **If there is no matching permit policy → access denied.**
914
969
 
915
970
  **What happens in this example:**
916
971
 
917
- 1. The `deny` policy exists, but its condition is **not met**
918
- → the policy gets status `mismatch`.
972
+ 1. The `deny` policy exists, but its condition is **not satisfied**
973
+ → the policy gets `mismatch` status.
919
974
 
920
- 2. The `deny` policy **is not applied** because the conditions did not match.
975
+ 2. The `deny` policy **does not apply** because the conditions did not match.
921
976
 
922
977
  3. There is no `permit` policy.
923
978
 
924
- 4. Since there is no permit policy → the final decision:
979
+ 4. Since there is no granting policy → final decision:
925
980
  **deny (by default)**.
926
981
 
927
982
  **Summary**
928
983
 
929
984
  - `deny` with matching conditions → **deny**
930
- - `deny` with nonmatching conditions → **deny (default deny)**
985
+ - `deny` with non-matching conditions → **deny (default deny)**
931
986
  - `permit` with matching conditions → **allow**
932
- - `permit` with nonmatching conditions → **deny (default deny)**
987
+ - `permit` with non-matching conditions → **deny (default deny)**
933
988
 
934
989
  **Conclusion**
935
990
 
936
- **Access is allowed only if there is an explicit permit.**
991
+ **Access is only granted when there is an explicit permit.**
937
992
 
938
- ## Design Recommendations
993
+ ## Design recommendations
939
994
 
940
- ### Naming Access Keys
995
+ ### Naming access keys
941
996
 
942
997
  - Use hierarchical keys: `permission.order.create`, `permission.order.update.status`, `permission.user.profile.update`.
943
998
  - Group by domains: `permission.user.*`, `permission.order.*`, `permission.product.*`.
944
999
  - Do not mix different domains in one key.
945
1000
 
946
- ### Data Structure
1001
+ ### Data structure
947
1002
 
948
1003
  - Explicitly describe `Resources` in TypeScript.
949
- - Do not pass extra fields this complicates understanding.
950
- - Strive to keep the data structure for a given `permission` stable.
1004
+ - Do not pass "extra" fields this complicates understanding.
1005
+ - Try to keep the data structure for a single `permission` stable.
951
1006
 
952
- ### Policy Design
1007
+ ### Designing policies
953
1008
 
954
- - General rules via wildcard (`permission.order.*`).
955
- - Specific restrictions via exact actions (`permission.order.update`).
1009
+ - Common rules via wildcard (`permission.order.*`).
1010
+ - Specific restrictions via exact actions (`permission.order.update`).
956
1011
  - Use `effect: deny` for prohibitions.
957
1012
  - Use `effect: permit` for permissions.
958
1013
 
959
- ### Common Mistakes
1014
+ ### Typical mistakes
960
1015
 
961
- - Expecting that absence of matching policies means allow.
1016
+ - Expecting that the absence of matching policies means deny.
962
1017
  - Mixing business logic and access policies.
963
- - Too large policies with dozens of rules better to break them down.
1018
+ - Too large policies with dozens of rules it's better to split them.
964
1019
 
965
- ### Example of Use on the Frontend (React)
1020
+ ### Example of use on the frontend (React)
966
1021
 
967
1022
  **Hook for checking policies**
968
1023
 
@@ -1006,7 +1061,7 @@ export function useAbility<Permission extends keyof Resources>(
1006
1061
  }
1007
1062
  ```
1008
1063
 
1009
- **Usage in a component**
1064
+ **Usage in component**
1010
1065
 
1011
1066
  ```tsx
1012
1067
  function OrderUpdateButton({ order, user }) {
@@ -1016,7 +1071,7 @@ function OrderUpdateButton({ order, user }) {
1016
1071
  });
1017
1072
 
1018
1073
  if (allowed === null) {
1019
- return null; // or loading spinner
1074
+ return null; // or loading badge
1020
1075
  }
1021
1076
 
1022
1077
  if (!allowed) {
@@ -1029,177 +1084,125 @@ function OrderUpdateButton({ order, user }) {
1029
1084
 
1030
1085
  ## Examples
1031
1086
 
1032
- ### Example of a Complex Multi‑Level Policy
1087
+ ### Example of a complex multi-stage policy
1033
1088
 
1034
- Below is a multi‑level set of policies, using a cinema example (fictional).
1089
+ Below is an example of a set of policies for a cinema.
1090
+ It demonstrates:
1035
1091
 
1036
- **The example demonstrates:**
1037
1092
  - working with roles (admin, seller, manager, VIP, banned),
1038
- - time constraints (`env.time.hour`),
1093
+ - time restrictions (`env.time.hour`),
1039
1094
  - wildcard permissions (`permission.*`),
1040
- - ticket quantity limits,
1095
+ - ticket quantity restrictions,
1041
1096
  - prohibition on selling already sold tickets,
1042
- - combination of `permit`/`deny` policies,
1043
- - policy priority and Default Deny model.
1044
-
1045
- **Brief description of rules**
1046
- - **Administrator**
1047
- Has wildcard permissions (`permission.*`) and can perform any action.
1048
- Can edit ticket prices.
1049
-
1050
- - **Seller**
1051
- Can sell tickets only during working hours (09:00–23:00).
1052
- Cannot sell tickets if:
1053
- - the cinema is closed,
1054
- - the ticket is already sold.
1055
-
1056
- - **Manager**
1057
- Has the same rights as a seller.
1097
+ - combination of `permit`/`deny`,
1098
+ - **sequential processing of policies**,
1099
+ - **state‑machine model**, where each policy can **set or reset the state**.
1058
1100
 
1059
- - **Buyers**
1060
- - A user older than 21 can buy tickets.
1061
- - A VIP user can buy tickets at any time.
1062
- - A banned user (`status = banned`) cannot buy tickets.
1063
- - Any user cannot buy more than 6 tickets.
1064
-
1065
- **Policy Diagram**
1066
-
1067
- ```mermaid
1068
- flowchart LR
1069
-
1070
- %% ==== ROLES ====
1071
-
1072
- subgraph Roles[Roles]
1073
- A[Administrator]
1074
- B[Seller]
1075
- C[Manager]
1076
- end
1077
-
1078
- subgraph Buyers[Buyers]
1079
- U1[User > 21]
1080
- U2[VIP user]
1081
- U3[Banned user]
1082
- end
1083
-
1084
- %% ==== ADMIN ====
1085
-
1086
- A --> A1[Wildcard: permission.*]
1087
- A --> A2[Edit ticket price]
1088
-
1089
- A1 --> FINAL[Final decision]
1090
- A2 --> FINAL
1101
+ ---
1091
1102
 
1092
- %% ==== SELLER ====
1103
+ **Unlike classic ABAC systems, where `mismatch` is ignored, Ability uses a `state‑machine` model:**
1093
1104
 
1094
- B --> B1[Sell tickets]
1105
+ - **match** policy sets a state (`allow` or `deny`)
1106
+ - **mismatch** → policy **resets the state to neutral**
1107
+ - the final result is determined by the **last processed policy**
1095
1108
 
1096
- B1 -->|09:00–23:00| B2[Allowed]
1097
- B1 -->|Outside hours| D2[Denied]
1098
- B1 -->|ticket.status = sold| D3[Denied]
1109
+ This means:
1099
1110
 
1100
- B2 --> FINAL
1101
- D2 --> FINAL
1102
- D3 --> FINAL
1111
+ - a policy can **override** the previous one
1112
+ - a policy can **cancel** the previous one (via mismatch)
1113
+ - the order of policies in DSL is critically important
1114
+ - the final decision does not always match the “intuitive” reading of rules from top to bottom
1103
1115
 
1104
- %% ==== MANAGER ====
1116
+ ### Brief description of the rules
1105
1117
 
1106
- C --> C1[Sell tickets as seller]
1107
- C1 --> FINAL
1118
+ **Administrator**
1108
1119
 
1109
- %% ==== BUYERS ====
1120
+ - Has wildcard rights (`permission.*`)
1121
+ - Can edit ticket prices
1110
1122
 
1111
- U1 --> U1A[Buy tickets]
1112
- U1A -->|ticketsCount < 6| U1OK[Allowed]
1113
- U1A -->|ticketsCount ≥ 6| U1DENY[Denied]
1123
+ **Seller**
1114
1124
 
1115
- U2 --> U2A[Buy tickets anytime]
1116
- U2A -->|ticketsCount < 6| U2OK[Allowed]
1117
- U2A -->|ticketsCount 6| U2DENY[Denied]
1125
+ - Can sell tickets only during working hours (09:00–23:00)
1126
+ - Cannot sell tickets if:
1127
+ - the cinema is closed,
1128
+ - the ticket is already sold
1118
1129
 
1119
- U3 --> U3A[Denied to buy tickets]
1130
+ **Manager**
1120
1131
 
1121
- U1OK --> FINAL
1122
- U1DENY --> FINAL
1123
- U2OK --> FINAL
1124
- U2DENY --> FINAL
1125
- U3A --> FINAL
1132
+ - Has the same rights as the seller
1126
1133
 
1127
- %% ==== DENY RULES ====
1134
+ **Buyers**
1128
1135
 
1129
- D1[Denied to buy tickets if user.status = banned] --> FINAL
1130
- ```
1136
+ - A user over 21 years old can buy tickets
1137
+ - A VIP user can buy tickets at any time
1138
+ - A banned user (`status = banned`) cannot buy tickets
1139
+ - Any user cannot buy more than 6 tickets
1131
1140
 
1132
- **DSL Policies**
1141
+ ### DSL policies
1133
1142
 
1134
1143
  ```dsl
1135
- ############################################################
1136
- # @name Admin can edit ticket price
1137
1144
  permit permission.ticket.price.edit if all:
1138
1145
  user.role is equals 'admin'
1139
1146
 
1140
-
1141
- ############################################################
1142
- # @name Seller can sell tickets during working hours
1143
1147
  permit permission.ticket.sell if all:
1144
1148
  user.role is equals 'seller'
1145
1149
  all of:
1146
1150
  env.time.hour greater than or equal 9
1147
1151
  env.time.hour less than or equal 23
1148
1152
 
1149
-
1150
- ############################################################
1151
- # @name Users older than 21 can buy tickets
1152
1153
  permit permission.ticket.buy if all:
1153
1154
  user.age greater than 21
1154
1155
 
1155
-
1156
- ############################################################
1157
- # @name VIP users can buy tickets anytime
1158
1156
  permit permission.ticket.buy if all:
1159
1157
  user.isVIP is true
1160
1158
 
1161
-
1162
- ############################################################
1163
- # @name Deny buying tickets if user is banned
1164
1159
  deny permission.ticket.buy if all:
1165
1160
  user.status is equals 'banned'
1166
1161
 
1167
-
1168
- ############################################################
1169
- # @name Deny selling tickets if cinema is closed
1170
1162
  deny permission.ticket.sell if all:
1171
1163
  any of:
1172
1164
  env.time.hour less than 9
1173
1165
  env.time.hour greater than 23
1174
1166
 
1175
-
1176
- ############################################################
1177
- # @name Manager can do everything seller can
1178
1167
  permit permission.ticket.sell if all:
1179
1168
  user.role is equals 'manager'
1180
1169
 
1181
-
1182
- ############################################################
1183
- # @name Admin wildcard permissions
1184
1170
  permit permission.* if all:
1185
1171
  user.role is equals 'admin'
1186
1172
 
1187
-
1188
- ############################################################
1189
- # @name Limit tickets per user (max 6)
1190
1173
  deny permission.ticket.buy if all:
1191
1174
  user.ticketsCount greater than or equal 6
1192
1175
 
1193
-
1194
- ############################################################
1195
- # @name Cannot sell already sold tickets
1196
1176
  deny permission.ticket.sell if all:
1197
1177
  ticket.status is equals 'sold'
1198
1178
  ```
1199
1179
 
1200
- Below is how to use the policies above in Node.js + TypeScript.
1180
+ **Example: seller sells a ticket at 3:00 PM**
1181
+
1182
+ 1. permit seller match → `allow`
1183
+ 2. deny closed mismatch → `neutral`
1184
+ 3. deny sold mismatch → `neutral`
1185
+
1186
+ Result: `neutral → deny`
1187
+
1188
+ **Example: VIP buys a ticket at night**
1189
+
1190
+ 1. permit age>21 mismatch → `neutral`
1191
+ 2. permit VIP match → `allow`
1192
+ 3. deny banned mismatch → `neutral`
1193
+ 4. deny limit mismatch → `neutral`
1194
+
1195
+ Result: `neutral → deny`
1196
+
1197
+ **Example: administrator sells a ticket at night**
1198
+
1199
+ 1. permit admin wildcard match → `allow`
1200
+ 2. deny closed match → `deny`
1201
+ 3. deny sold mismatch → `neutral`
1202
+
1203
+ Result: `neutral → deny`
1201
1204
 
1202
- **Preparing Policies**
1205
+ ### Preparing policies
1203
1206
 
1204
1207
  ```ts
1205
1208
  import { AbilityDSLParser } from '@via-profit/ability';
@@ -1208,7 +1211,7 @@ import cinemaDSL from './policies/cinema.dsl';
1208
1211
  export const policies = new AbilityDSLParser(cinemaDSL).parse();
1209
1212
  ```
1210
1213
 
1211
- **Creating the Resolver**
1214
+ ### Creating a resolver
1212
1215
 
1213
1216
  ```ts
1214
1217
  import { AbilityResolver } from '@via-profit/ability';
@@ -1217,11 +1220,7 @@ import { policies } from './policies';
1217
1220
  const resolver = new AbilityResolver(policies);
1218
1221
  ```
1219
1222
 
1220
- **Checking Permissions (enforce)**
1221
-
1222
- Example: buying a ticket.
1223
-
1224
- The `enforce` method throws an `AbilityError` if access is denied.
1223
+ ### enforce (throws an error on deny)
1225
1224
 
1226
1225
  ```ts
1227
1226
  resolver.enforce('ticket.buy', {
@@ -1229,12 +1228,8 @@ resolver.enforce('ticket.buy', {
1229
1228
  env: { time: { hour: 18 } },
1230
1229
  });
1231
1230
  ```
1232
- If allowed — the code continues execution.
1233
- If denied — an `AbilityError` exception is thrown.
1234
-
1235
- **Checking Permissions Without Exceptions (resolve)**
1236
1231
 
1237
- `resolve` returns a result object:
1232
+ ### resolve (without exceptions)
1238
1233
 
1239
1234
  ```ts
1240
1235
  const result = resolver.resolve('ticket.buy', {
@@ -1249,19 +1244,9 @@ if (result.isAllowed()) {
1249
1244
  }
1250
1245
  ```
1251
1246
 
1252
- **Seller can only sell during working hours**
1247
+ **Preparing data for the resolver**
1253
1248
 
1254
- ```ts
1255
- resolver.enforce('ticket.sell', {
1256
- user: { role: 'seller' },
1257
- env: { time: { hour: 15 } },
1258
- ticket: { status: 'available' },
1259
- });
1260
- ```
1261
-
1262
- **Preparing Data for the Resolver**
1263
-
1264
- In the examples above, constant objects are passed to the resolver:
1249
+ In the examples above, simple constant objects are passed to the resolver:
1265
1250
 
1266
1251
  ```ts
1267
1252
  resolver.enforce('ticket.buy', {
@@ -1270,7 +1255,7 @@ resolver.enforce('ticket.buy', {
1270
1255
  });
1271
1256
  ```
1272
1257
 
1273
- This is done for clarity. In a real application, the data for the resolver should be built dynamically from the sources available to your server.
1258
+ 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.
1274
1259
 
1275
1260
  **User** (`user`) is usually taken from:
1276
1261
 
@@ -1285,7 +1270,7 @@ Example:
1285
1270
  const user = await db.users.findById(session.userId);
1286
1271
  ```
1287
1272
 
1288
- **Environment** (`env`)
1273
+ **Environment (`env`)**
1289
1274
 
1290
1275
  These are any external parameters that can affect access:
1291
1276
 
@@ -1308,7 +1293,7 @@ const env = {
1308
1293
 
1309
1294
  **Resource** (e.g., `ticket`)
1310
1295
 
1311
- If the action is associated with a specific object, it also needs to be loaded:
1296
+ If the action is related to a specific object it also needs to be loaded:
1312
1297
 
1313
1298
  ```ts
1314
1299
  const ticket = await db.tickets.findById(req.params.ticketId);
@@ -1316,17 +1301,17 @@ const ticket = await db.tickets.findById(req.params.ticketId);
1316
1301
 
1317
1302
  **Context**
1318
1303
 
1319
- Context is the object that you pass to `resolve` or `enforce`.
1320
- It contains **all the data** that policies might need:
1304
+ The context is the object you pass to `resolve` or `enforce`.
1305
+ It contains **all the data** that policies may need:
1321
1306
 
1322
- - `user` data about the current user
1323
- - `env` environment data (time, IP, geography, system settings)
1324
- - `resource` or `ticket` data about the entity on which the action is performed
1325
- - any other objects that you use in DSL
1307
+ - `user` data about the current user
1308
+ - `env` environment data (time, IP, geography, system settings)
1309
+ - `resource` or `ticket` data about the entity on which the action is performed
1310
+ - any other objects you use in the DSL
1326
1311
 
1327
- **It is important to understand:**
1312
+ **Important to understand:**
1328
1313
 
1329
- > Context is formed for a specific action and specific policies. It does not need to be stored in advance you gather it dynamically before calling the resolver.
1314
+ > 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.
1330
1315
 
1331
1316
  ## Performance
1332
1317
 
@@ -1361,3 +1346,4 @@ Throughput (ops/s)
1361
1346
  ## License
1362
1347
 
1363
1348
  This project is licensed under the MIT License. See the [LICENSE](/LICENSE) file for details.
1349
+ ```