@via-profit/ability 3.5.0 → 3.5.2

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