@via-profit/ability 3.1.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +384 -414
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,35 +1,38 @@
1
1
  # @via-profit/Ability
2
2
 
3
- > Набор сервисов, частично реализующих
4
- > принцип [Attribute Based Access Control](https://en.wikipedia.org/wiki/Attribute-based_access_control)
5
- > Пакет позволяет описывать правила, объединять их в группы, формировать политики и применять их к данным для определения разрешений.
3
+ > A set of services that partially implement the [Attribute Based Access Control](https://en.wikipedia.org/wiki/Attribute-based_access_control) principle.
4
+ > The package allows you to describe rules, combine them into groups, form policies, and apply them to data to determine permissions.
6
5
 
7
- ## Для чего
6
+ ## Language / Язык
8
7
 
9
- Пакет задуман как **лёгкая и предельно простая альтернатива** тяжёлым системам управления доступом.
10
- Без сложных конфигураций, без зависимостей — только минимальный набор инструментов, который позволяет описывать правила и политики в максимально простом DSL.
8
+ - [🇬🇧 English](/docs/en/README.md)
9
+ - [🇷🇺 Русский](/docs/ru/README.md)
11
10
 
12
- ## Содержание
11
+ ## Purpose
13
12
 
14
- - [Быстрый старт](#быстрый-старт)
15
- - [Основные положения](#основные-положения)
16
- - [DSL](#dsl)
17
- - [Объединение политик](#объединение-политик)
18
- - [Environment политик](#environment-политик)
19
- - [Генератор типов для TypeScript](#генератор-типов-для-typescript)
20
- - [Отладка политик](#отладка-политик)
21
- - [Решение проблем](#решение-проблем)
22
- - [Рекомендации по проектированию](#рекомендации-по-проектированию)
23
- - [Примеры](#примеры)
24
- - [Производительность](#производительность)
25
- - [Api-Reference](./docs/ru/api.md)
13
+ The package is intended as a **lightweight and extremely simple alternative** to heavy access control systems.
14
+ Without complex configurations, without dependencies — just a minimal set of tools that allows you to describe rules and policies in a maximally simple DSL.
15
+
16
+ ## Table of Contents
26
17
 
18
+ - [Quick Start](#quick-start)
19
+ - [Fundamentals](#fundamentals)
20
+ - [DSL](#dsl)
21
+ - [Combining Policies](#combining-policies)
22
+ - [Policy Environment](#policy-environment)
23
+ - [TypeScript Type Generator](#typescript-type-generator)
24
+ - [Policy Debugging](#policy-debugging)
25
+ - [Troubleshooting](#troubleshooting)
26
+ - [Design Recommendations](#design-recommendations)
27
+ - [Examples](#examples)
28
+ - [Performance](#performance)
29
+ - [API Reference](./api.md)
27
30
 
28
- ## Быстрый старт
31
+ ## Quick Start
29
32
 
30
- Установить пакет, написать DSL, вызвать парсер, запустить резолвер.
33
+ Install the package, write DSL, call the parser, and run the resolver.
31
34
 
32
- ### Установка
35
+ ### Installation
33
36
 
34
37
  ```bash
35
38
  npm install @via-profit/ability
@@ -43,10 +46,9 @@ yarn add @via-profit/ability
43
46
  pnpm add @via-profit/ability
44
47
  ```
45
48
 
49
+ ### Example: Deny access to `passwordHash` for everyone except the owner
46
50
 
47
- ### Пример: запретить доступ к `passwordHash` всем, кроме владельца
48
-
49
- Допустим, у нас есть пользовательские данные:
51
+ Suppose we have user data:
50
52
 
51
53
  ```ts
52
54
  const user = {
@@ -56,30 +58,29 @@ const user = {
56
58
  };
57
59
  ```
58
60
 
59
- Нужно запретить чтение `passwordHash` всем, кроме самого пользователя.
61
+ We need to deny reading `passwordHash` to everyone except the user themselves.
60
62
 
61
- #### DSL‑политика
63
+ #### DSL Policy
62
64
 
63
- На языке политик это выглядит так:
65
+ In the policy language, this looks like:
64
66
 
65
67
  ```
66
68
  deny permission.user.passwordHash if any:
67
69
  viewer.id is not equals owner.id
68
70
  ```
69
71
 
70
- **Пояснение:**
71
-
72
- - `deny` — эффект политики (запретить доступ)
73
- - `permission.user.passwordHash` — ключ разрешения.
74
- - `if any:` — начало блока условий
75
- - `viewer.id is not equals owner.id` — правило: если идентификатор запрашивающего не равен идентификатору владельца
72
+ **Explanation:**
76
73
 
74
+ - `deny` — policy effect (deny access)
75
+ - `permission.user.passwordHash` — permission key.
76
+ - `if any:` — start of the condition block
77
+ - `viewer.id is not equals owner.id` — rule: if the requester's ID is not equal to the owner's ID
77
78
 
78
- Если `viewer.id` не равен `owner.id`, правило считается выполненным, и политика возвращает `deny` — доступ запрещён. Если же идентификаторы совпадают (т.е. пользователь запрашивает свои собственные данные), правило не срабатывает, и доступ разрешается.
79
+ 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.
79
80
 
80
- _Замечание: Ключ разрешения формируется по принципу: `permission.` + ваш кастомный ключ в формате **dot notation**, например, ключ `foo.bar.baz` в DSL будет иметь вид `permission.foo.bar.baz`_
81
+ *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`.*
81
82
 
82
- #### Проверка в коде
83
+ #### Check in Code
83
84
 
84
85
  ```ts
85
86
  import { AbilityDSLParser, AbilityResolver } from '@via-profit/ability';
@@ -89,28 +90,28 @@ deny permission.user.passwordHash if any:
89
90
  viewer.id is not equals owner.id
90
91
  `;
91
92
 
92
- const policies = new AbilityDSLParser(dsl).parse(); // получение политик
93
- const resolver = new AbilityResolver(policies); // создание резолвера
93
+ const policies = new AbilityDSLParser(dsl).parse(); // obtain policies
94
+ const resolver = new AbilityResolver(policies); // create resolver
94
95
 
95
96
  resolver.enforce('user.passwordHash', {
96
97
  viewer: { id: '1' },
97
98
  owner: { id: '2' },
98
- }); // выбросит ошибкудоступ запрещён
99
+ }); // will throw an error access denied
99
100
  ```
100
- В `enforce` передаётся ключ без префикса `permission.` — он автоматически удаляется парсером.
101
+ In `enforce`, the key is passed without the `permission.` prefix it is automatically removed by the parser.
101
102
 
102
- ## Основные положения
103
+ ## Fundamentals
103
104
 
104
- Тезисно перечислим основные положения, которые необходимо знать перед тем как начать пользоваться пакетом:
105
+ Let’s briefly list the key points you need to know before starting to use the package:
105
106
 
106
- 1. Резолвер (`AbilityResolver`) настроен по принципу `Default Deny`. Это значит, что если ни одна политика не сработала, то результат будет `deny` ([подробнее здесь](#решение-проблем)). Чтобы избежать неожиданного `deny`, убедитесь, что существует хотя бы одна `permit`‑политика, которая может совпасть. Только после этого добавляйте `deny`‑политики.
107
- 2. Политики применяются последовательно. Если несколько политик совпали, результат определяется последней совпавшей политикой.
108
- 3. Правила выполняются последовательно.
109
- 4. В группе правил (`RuleSet`) с оператором сравнения `all` дальнейшее выполнение правил прекращается как только первое же правило вернёт `mismatch`.
110
- 5. Для составления политик используйте [DSL](#dsl) — это проще и удобнее
111
- 6. Для хранения политик на сервере используйте JSON. Политики возможно экспортировать в JSON и импортировать из JSON
112
- 7. Чаще всего следует опираться на утверждение если разрешение не выдано явнодоступ запрещён.
113
- 8. Используйте встроенный кэш только в случаях, если ваши политики неимоверно сложны и содержат большое количество правил
107
+ 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.
108
+ 2. Policies are applied sequentially. If multiple policies match, the result is determined by the last matching policy.
109
+ 3. Rules are executed sequentially.
110
+ 4. In a rule set (`RuleSet`) with the `all` comparison operator, further rule execution stops as soon as the first rule returns `mismatch`.
111
+ 5. Use [DSL](#dsl) to compose policies it's simpler and more convenient.
112
+ 6. For storing policies on the server, use JSON. Policies can be exported to JSON and imported from JSON.
113
+ 7. Generally, rely on the principle: if permission is not explicitly grantedaccess is denied.
114
+ 8. Use the built-in cache only if your policies are incredibly complex and contain a large number of rules.
114
115
 
115
116
  ---
116
117
 
@@ -118,28 +119,28 @@ resolver.enforce('user.passwordHash', {
118
119
 
119
120
  > DSL - Domain-Specific Language
120
121
 
121
- Ability DSL это декларативный язык для описания политик доступа.
122
- Он позволяет определять правила в человекочитаемой форме, используя простые конструкции: *политики*, *группы*, *правила* и *аннотации*.
122
+ Ability DSL is a declarative language for describing access policies.
123
+ It allows you to define rules in a human-readable form using simple constructs: *policies*, *groups*, *rules*, and *annotations*.
123
124
 
124
- ### Структура политики
125
+ ### Policy Structure
125
126
 
126
- Политика состоит из:
127
+ A policy consists of:
127
128
 
128
129
  ```
129
130
  <effect> <permission> if <all|any>:
130
131
  <group>...
131
132
  ```
132
133
 
133
- Где:
134
+ Where:
134
135
 
135
- - **effect** — `permit` или `deny`
136
- - **permission** — строка вида `permission.foo.bar`, где суффикс `permission.` обязателен.
137
- - **if all:** — все группы должны быть истинны
138
- - **if any:** — хотя бы одна группа должна быть истинна
136
+ - **effect** — `permit` or `deny`
137
+ - **permission** — a string of the form `permission.foo.bar`, where the `permission.` prefix is mandatory.
138
+ - **if all:** — all groups must be true
139
+ - **if any:** — at least one group must be true
139
140
 
140
- Политика может содержать одну или несколько групп правил.
141
+ A policy can contain one or more rule groups.
141
142
 
142
- Пример:
143
+ Example:
143
144
 
144
145
  ```dsl
145
146
  permit permission.order.update if any:
@@ -152,38 +153,36 @@ permit permission.order.update if any:
152
153
  user.login is equals 'dev'
153
154
  ```
154
155
 
155
- > Префикс `permission.` обязателен в DSL, но автоматически удаляется парсером. Внутри системы разрешение хранится как `order.update`.
156
+ > The `permission.` prefix is mandatory in DSL but is automatically removed by the parser. Internally, the permission is stored as `order.update`.
156
157
 
157
- Пример политики выше гласит - разрешение `permission.order.update` будет разрешено при выполнении одного из двух условий:
158
- 1. user.roles содержит 'admin' **и** user.token не null
159
- 2. user.roles содержит 'developer' **или** user.login равен 'dev'
158
+ The example policy above says: permission `order.update` will be allowed if one of two conditions is met:
159
+ 1. `user.roles` contains 'admin' **and** `user.token` is not null
160
+ 2. `user.roles` contains 'developer' **or** `user.login` equals 'dev'
160
161
 
161
- ### Ключ разрешения (permission key)
162
+ ### Permission Key
162
163
 
163
- Ключ разрешения записываются в `dot notation` виде, но поддерживают возможность использования wildcard шаблонов при
164
- помощи символа `*`. Это позволяет группировать ключи, а так же переопределять политики с похожими ключами.
164
+ 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.
165
165
 
166
- Если под ключ подходит несколько политик, **выполняются все**. Итог определяется **последней совпавшей политикой**:
166
+ If multiple policies match a key, **all of them are executed**. The final result is determined by the **last matching policy**:
167
167
 
168
+ **Example of using wildcards**
168
169
 
169
- **Пример использования шаблонов**
170
+ | Policy (permission) | Key | Matches |
171
+ |---------------------|-----------------------|---------|
172
+ | `order.*` | `order.create` | yes |
173
+ | `order.*` | `order.update` | yes |
174
+ | `order.*` | `user.create` | no |
175
+ | `*.create` | `order.create` | yes |
176
+ | `*.create` | `user.create` | yes |
177
+ | `*.create` | `order.update` | no |
178
+ | `user.profile.*` | `user.profile.update` | yes |
179
+ | `user.profile.*` | `user.settings.update`| no |
170
180
 
171
- | Политика (permission) | ключ | Совпадает |
172
- |-------------------|------------------------|-----------|
173
- | `order.*` | `order.create` | да |
174
- | `order.*` | `order.update` | да |
175
- | `order.*` | `user.create` | нет |
176
- | `*.create` | `order.create` | да |
177
- | `*.create` | `user.create` | да |
178
- | `*.create` | `order.update` | нет |
179
- | `user.profile.*` | `user.profile.update` | да |
180
- | `user.profile.*` | `user.settings.update` | нет |
181
-
182
- **Пример политики с wildcard**
181
+ **Example of a policy with wildcard**
183
182
  ```ts
184
183
  import { AbilityDSLParser, AbilityResolver } from '@via-profit/ability';
185
184
 
186
- // DSL не полный и показан только ради примера
185
+ // DSL is not complete, shown for illustration only
187
186
  const dsl = `
188
187
  permit permission.order.*
189
188
  deny permission.order.update
@@ -192,21 +191,20 @@ deny permission.order.update
192
191
  const policies = new AbilityDSLParser(dsl).parse();
193
192
  const resolver = new AbilityResolver(policies);
194
193
 
195
- await resolver.enforce('order.update', resource); // выбросит AbilityError
196
-
194
+ await resolver.enforce('order.update', resource); // will throw AbilityError
197
195
  ```
198
196
 
199
- **Пояснение**
197
+ **Explanation**
200
198
 
201
- В DSL порядок политик имеет значение:
202
- последняя совпавшая политика выигрывает.
199
+ In DSL, the order of policies matters:
200
+ the last matching policy wins.
203
201
 
204
- Поэтому:
202
+ Therefore:
205
203
 
206
- 1. `permit` `permission.order.*` разрешает всё, что начинается с `order.`
207
- 2. `deny` `permission.order.update` перекрывает это разрешение.
204
+ 1. `permit` `permission.order.*` allows everything that starts with `order.`
205
+ 2. `deny` `permission.order.update` overrides this permission.
208
206
 
209
- Итог выполнения:
207
+ Execution result:
210
208
 
211
209
  ```
212
210
  order.update → deny
@@ -215,30 +213,29 @@ order.delete → permit
215
213
  order.view → permit
216
214
  ```
217
215
 
216
+ ### Comments
218
217
 
219
- ### Комментарии
220
-
221
- Строки, начинающиеся с символа `#` считаются комментариями и не влияют на результат работы правил и политик.
218
+ Lines starting with the `#` symbol are considered comments and do not affect the evaluation of rules and policies.
222
219
 
223
220
  ---
224
221
 
225
- ### Аннотации
222
+ ### Annotations
226
223
 
227
- В настоящий момент поддерживается только одна аннотация ’name’, которая будет использована в качестве имени для политики, либо группы правил, либо правила.
224
+ Currently, only one annotation is supported: `name`, which will be used as the name for a policy, rule group, or rule.
228
225
 
229
- Аннотации задаются через комментарии:
226
+ Annotations are specified via comments:
230
227
 
231
228
  ```
232
- # @name <имя>
229
+ # @name <name>
233
230
  ```
234
231
 
235
- Аннотации применяются к **следующей сущности**:
232
+ Annotations apply to the **following entity**:
236
233
 
237
- - политике
238
- - группе
239
- - правилу
234
+ - policy
235
+ - group
236
+ - rule
240
237
 
241
- Пример:
238
+ Example:
242
239
 
243
240
  ```dsl
244
241
  # @name can order update
@@ -251,9 +248,9 @@ permit permission.order.update if any:
251
248
 
252
249
  ---
253
250
 
254
- ### Группы правил
251
+ ### Rule Groups
255
252
 
256
- Группа определяет, как объединяются правила внутри неё:
253
+ A group defines how the rules within it are combined:
257
254
 
258
255
  ```
259
256
  all of:
@@ -265,17 +262,16 @@ any of:
265
262
  <rule>
266
263
  ```
267
264
 
268
- - `all of:` — логическое AND
269
- - `any of:` — логическое OR
270
-
271
- `all of` - значит, что группа считается выполненной, если все правила внутри группы сработали.
265
+ - `all of:` — logical AND
266
+ - `any of:` — logical OR
272
267
 
273
- `any of` - значит, что группа считается выполненной, если хотя бы одно правило внутри группы сработало.
268
+ `all of` means that the group is considered satisfied if all rules within the group match.
274
269
 
275
- Каждая группа внутри политики будет вычисляться независимо от других групп. Итоговая оценка результата будет определена путем сравнения результата вычисления всех групп в политике.
270
+ `any of` means that the group is considered satisfied if at least one rule within the group matches.
276
271
 
272
+ 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.
277
273
 
278
- Группы могут иметь аннотации:
274
+ Groups can have annotations:
279
275
 
280
276
  ```dsl
281
277
  # @name developer group
@@ -285,19 +281,19 @@ any of:
285
281
 
286
282
  ---
287
283
 
288
- ### Правила
284
+ ### Rules
289
285
 
290
- Правило это атомарное условие внутри политики. Оно определяет, при каких данных политика будет считаться совпавшей. С помощью правил задаются условия по которым определяется эффективность политики (`permit` или `deny`)
286
+ 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`).
291
287
 
292
- Правило имеет форму:
288
+ A rule has the form:
293
289
 
294
290
  ```
295
- <subject> <operator> <value?> — значение указывается не для всех операторов (например, is null не требует значения).
291
+ <subject> <operator> <value?> — the value is not required for some operators (e.g., `is null` does not require a value).
296
292
  ```
297
293
 
298
- #### Subject (субъект)
294
+ #### Subject
299
295
 
300
- Идентификатор в dot‑нотации:
296
+ Identifier in dot notation:
301
297
 
302
298
  ```
303
299
  user.roles
@@ -305,101 +301,97 @@ env.time.hour
305
301
  order.total
306
302
  ```
307
303
 
308
- #### Operators (операторы)
304
+ #### Operators
309
305
 
310
- _Синонимы это альтернативные формы записи, которые также поддерживаются парсером._
306
+ *Synonyms are alternative forms of writing that are also supported by the parser.*
311
307
 
312
- **Базовые операторы сравнения**
308
+ **Basic Comparison Operators**
313
309
 
314
- | Оператор DSL | Синонимы | Пример | Описание | Типы |
315
- |--------------|----------|--------|----------|------|
316
- | **is equals** | `=`, `==`, `equals` | `age is equals 18` | Строгое равенство | number, string, boolean |
317
- | **is not equals** | `!=`, `<>`, `not equals` | `role is not equals 'admin'` | Строгое неравенство | number, string, boolean |
318
- | **greater than** | `>`, `gt` | `age greater than 18` | Больше | number, date |
319
- | **greater than or equal** | `>=`, `gte` | `age greater than or equal 18` | Больше или равно | number, date |
320
- | **less than** | `<`, `lt` | `age less than 18` | Меньше | number, date |
321
- | **less than or equal** | `<=`, `lte` | `age less than or equal 18` | Меньше или равно | number, date |
310
+ | DSL Operator | Synonyms | Example | Description | Types |
311
+ |--------------|----------|---------|-------------|-------|
312
+ | **is equals** | `=`, `==`, `equals` | `age is equals 18` | Strict equality | number, string, boolean |
313
+ | **is not equals** | `!=`, `<>`, `not equals` | `role is not equals 'admin'` | Strict inequality | number, string, boolean |
314
+ | **greater than** | `>`, `gt` | `age greater than 18` | Greater than | number, date |
315
+ | **greater than or equal** | `>=`, `gte` | `age greater than or equal 18` | Greater than or equal | number, date |
316
+ | **less than** | `<`, `lt` | `age less than 18` | Less than | number, date |
317
+ | **less than or equal** | `<=`, `lte` | `age less than or equal 18` | Less than or equal | number, date |
322
318
 
319
+ **Null Operators**
323
320
 
324
- **Null‑операторы**
321
+ | DSL Operator | Synonyms | Example | Description | Types |
322
+ |--------------|----------|---------|-------------|-------|
323
+ | **is null** | `== null`, `= null` | `middleName is null` | Value is absent | any |
324
+ | **is not null** | `!= null` | `middleName is not null` | Value is present | any |
325
325
 
326
- | Оператор DSL | Синонимы | Пример | Описание | Типы |
327
- |--------------|----------|--------|----------|------|
328
- | **is null** | `== null`, `= null` | `middleName is null` | Значение отсутствует | any |
329
- | **is not null** | `!= null` | `middleName is not null` | Значение присутствует | any |
326
+ **Operators for Lists (Arrays)**
330
327
 
331
- **Операторы для списков (массивов)**
328
+ | DSL Operator | Synonyms | Example | Description | Types |
329
+ |--------------|---------------------------|---------|-------------|-------|
330
+ | **in [...]** | - | `role in ['admin', 'manager']` | Value is in the list | number, string |
331
+ | **not in [...]** | - | `role not in ['banned']` | Value is not in the list | number, string |
332
+ | **contains** | `includes`, `has` | `tags contains 'vip'` | Array contains the element | array |
333
+ | **not contains** | `not includes`, `not has` | `tags not contains 'vip'` | Array does not contain the element | array |
332
334
 
333
- | Оператор DSL | Синонимы | Пример | Описание | Типы |
334
- |--------------|---------------------------|--------|----------|------|
335
- | **in [...]** | - | `role in ['admin', 'manager']` | Значение входит в список | number, string |
336
- | **not in [...]** | - | `role not in ['banned']` | Значение не входит | number, string |
337
- | **contains** | `includes`, `has` | `tags contains 'vip'` | Массив содержит элемент | array |
338
- | **not contains** | `not includes`, `not has` | `tags not contains 'vip'` | Массив не содержит элемент | array |
335
+ **String Operators**
339
336
 
337
+ | DSL Operator | Synonyms | Example | Description | Types |
338
+ |--------------|----------|---------|-------------|-------|
339
+ | **starts with** | `begins with` | `email starts with 'admin@'` | String starts with | string |
340
+ | **not starts with** | — | `email not starts with 'test'` | String does not start with | string |
341
+ | **ends with** | — | `email ends with '.ru'` | String ends with | string |
342
+ | **not ends with** | — | `email not ends with '.com'` | String does not end with | string |
343
+ | **includes** | `contains substring` | `name includes 'lex'` | String contains substring | string |
344
+ | **not includes** | — | `name not includes 'test'` | String does not contain substring | string |
340
345
 
341
- **Строковые операторы**
346
+ **Boolean Operators**
342
347
 
343
- | Оператор DSL | Синонимы | Пример | Описание | Типы |
344
- |--------------|----------|--------|----------|------|
345
- | **starts with** | `begins with` | `email starts with 'admin@'` | Строка начинается с | string |
346
- | **not starts with** | | `email not starts with 'test'` | Строка не начинается с | string |
347
- | **ends with** | — | `email ends with '.ru'` | Строка заканчивается на | string |
348
- | **not ends with** | — | `email not ends with '.com'` | Строка не заканчивается на | string |
349
- | **includes** | `contains substring` | `name includes 'lex'` | Строка содержит подстроку | string |
350
- | **not includes** | — | `name not includes 'test'` | Строка не содержит подстроку | string |
348
+ | DSL Operator | Synonyms | Example | Description | Types |
349
+ |--------------|----------|---------|-------------|-------|
350
+ | **is true** | `= true` | `isActive is true` | Value is true | boolean |
351
+ | **is false** | `= false` | `isActive is false` | Value is false | boolean |
351
352
 
352
- **Булевые операторы**
353
+ **Length Operators**
353
354
 
354
- | Оператор DSL | Синонимы | Пример | Описание | Типы |
355
- |--------------|----------|--------|----------|------|
356
- | **is true** | `= true` | `isActive is true` | Значение истинно | boolean |
357
- | **is false** | `= false` | `isActive is false` | Значение ложно | boolean |
355
+ | DSL Operator | Synonyms | Example | Description | Types |
356
+ |--------------|----------|---------|-------------|-------|
357
+ | **length equals** | `len =` | `tags length equals 3` | Length equals | array, string |
358
+ | **length greater than** | `len >` | `tags length greater than 2` | Length greater than | array, string |
359
+ | **length less than** | `len <` | `tags length less than 5` | Length less than | array, string |
358
360
 
359
- **Операторы длины**
361
+ #### Value
360
362
 
361
- | Оператор DSL | Синонимы | Пример | Описание | Типы |
362
- |--------------|----------|--------|----------|------|
363
- | **length equals** | `len =` | `tags length equals 3` | Длина равна | array, string |
364
- | **length greater than** | `len >` | `tags length greater than 2` | Длина больше | array, string |
365
- | **length less than** | `len <` | `tags length less than 5` | Длина меньше | array, string |
363
+ Supported values:
366
364
 
367
- #### Value (значение)
368
-
369
- Поддерживаются:
370
-
371
- - строки `'text'`
372
- - числа `42`
373
- - булевы `true` / `false`
365
+ - strings `'text'`
366
+ - numbers `42`
367
+ - booleans `true` / `false`
374
368
  - `null`
375
- - массивы `[1, 2, 3]` / `['foo', false, null, 1, 2, '999']`
369
+ - arrays `[1, 2, 3]` / `['foo', false, null, 1, 2, '999']`
376
370
 
377
- Примеры:
371
+ Examples:
378
372
 
379
373
  ```dsl
380
- # возраст пользователя больше 18
374
+ # user age greater than 18
381
375
  user.age greater than 18
382
376
 
383
- # массив ролей содержит роль 'admin'
377
+ # array of roles contains the role 'admin'
384
378
  user.roles contains 'admin'
385
379
 
386
- # тэг заказа либо 'vip', либо 'priority'
380
+ # order tag is either 'vip' or 'priority'
387
381
  order.tag in ['vip', 'priority']
388
382
 
389
- # токен пользователя не null
383
+ # user token is not null
390
384
  user.token is not null
391
385
 
392
- # логин пользователя длиннее 12 символов
386
+ # user login is longer than 12 characters
393
387
  user.login length greater than 12
394
388
  ```
395
389
 
396
-
397
-
398
390
  ---
399
391
 
400
- ### Неявная группа (implicit group)
392
+ ### Implicit Group
401
393
 
402
- Если правила идут без `all of:` или `any of:`, они объединяются оператором политики:
394
+ If rules are written without `all of:` or `any of:`, they are combined using the policy operator:
403
395
 
404
396
  ```dsl
405
397
  permit permission.order.update if all:
@@ -407,7 +399,7 @@ permit permission.order.update if all:
407
399
  user.token is not null
408
400
  ```
409
401
 
410
- Эквивалентно:
402
+ Equivalent to:
411
403
 
412
404
  ```dsl
413
405
  permit permission.order.update if all:
@@ -416,48 +408,46 @@ permit permission.order.update if all:
416
408
  user.token is not null
417
409
  ```
418
410
 
419
- Неявная группа всегда соответствует оператору политики (`if all` или `if any`).
411
+ The implicit group always matches the policy operator (`if all` or `if any`).
420
412
 
421
413
  ---
422
414
 
423
- ### Полный пример
415
+ ### Complete Example
424
416
 
425
417
  ```dsl
426
- # @name разрешено обновление заказа
418
+ # @name order update allowed
427
419
  permit permission.order.update if any:
428
420
 
429
- # @name если это администратор
421
+ # @name if admin
430
422
  all of:
431
423
  user.roles contains 'admin'
432
424
  user.token is not null
433
425
 
434
- # @name если это разработчик
426
+ # @name if developer
435
427
  any of:
436
428
  user.roles contains 'developer'
437
429
  user.login is equals 'dev'
438
430
  ```
439
431
 
432
+ ## Combining Policies
440
433
 
434
+ In a real project, you should use multiple policies at once.
441
435
 
442
- ## Объединение политик
443
-
444
- В реальном проекте следует использовать несколько политик сразу
436
+ TODO: using multiple policies
445
437
 
446
- TODO: использование нескольких политик
438
+ ## Policy Environment
447
439
 
448
- ## Environment политик
440
+ **Environment** is an object containing context data that does not belong to either the user or the resource.
441
+ The content of the object is defined by the developer and can be any object consisting of primitives.
449
442
 
450
- **Environment** это объект, содержащий данные окружения, которые не принадлежат ни пользователю, ни ресурсу.
451
- Содержимое объекта определяется разработчиком и может быть любым объектом состоящим из примитивов.
443
+ - request time,
444
+ - IP address,
445
+ - device parameters,
446
+ - request headers,
447
+ - session context,
448
+ - any other external conditions.
452
449
 
453
- - время запроса,
454
- - IP‑адрес,
455
- - параметры устройства,
456
- - заголовки запроса,
457
- - контекст сессии,
458
- - любые другие внешние условия.
459
-
460
- **Примеры:**
450
+ **Examples:**
461
451
 
462
452
  ```ts
463
453
  type Environment = {
@@ -471,18 +461,18 @@ type Environment = {
471
461
  };
472
462
  ```
473
463
 
474
- Environment передаётся в `resolve()` и `enforce()` как третий аргумент:
464
+ Environment is passed to `resolve()` and `enforce()` as the third argument:
475
465
 
476
466
  ```ts
477
467
  await resolver.resolve('order.update', resource, environment);
478
468
  await resolver.enforce('order.update', resource, environment);
479
469
  ```
480
470
 
481
- ### Использование environment в правилах
471
+ ### Using environment in rules
482
472
 
483
- В политике можно ссылаться на environment через путь `env.*`.
473
+ In a policy, you can refer to environment via the `env.*` path.
484
474
 
485
- Пример политики, которая запрещает обновление заказов ночью (22:0006:00).:
475
+ Example policy that denies order updates at night (10 PM 6 AM):
486
476
 
487
477
  ```dsl
488
478
  # @name Deny updates at night
@@ -491,15 +481,15 @@ deny permission.order.update if all:
491
481
  env.time.hour greater or equal than 22
492
482
  ```
493
483
 
494
- **Извлечение значений из environment**
484
+ **Retrieving values from environment**
495
485
 
496
- Если в правиле указан путь:
486
+ If a path is specified in a rule:
497
487
 
498
- - `env.*` → значение берётся из environment
499
- - `user.*`, `order.*`, `profile.*` → из resource
500
- - литерал (`18`, `"admin"`, `true`) → используется как есть
488
+ - `env.*` → value is taken from environment
489
+ - `user.*`, `order.*`, `profile.*` → from resource
490
+ - literal (`18`, `"admin"`, `true`) → used as is
501
491
 
502
- Пример:
492
+ Example:
503
493
 
504
494
  ```ts
505
495
  subject: "env.geo.country"
@@ -507,31 +497,29 @@ resource: "user.country"
507
497
  condition: "equal"
508
498
  ```
509
499
 
510
- ### Environment в TypeScript
500
+ ### Environment in TypeScript
511
501
 
512
- Тип Environment задаётся на уровне `AbilityResolver`:
502
+ The Environment type is set at the `AbilityResolver` level:
513
503
 
514
504
  ```ts
515
505
  const resolver = new AbilityResolver<Resources, Environment>(policies);
516
506
  ```
517
507
 
518
- Это позволяет:
519
-
520
- - получать автодополнение в IDE,
521
- - проверять корректность путей `env.*`,
522
- - избегать ошибок при передаче environment.
508
+ This allows:
523
509
 
524
- > Если правило использует `env.*`, но environment не передан, то значение `env.*` будет `undefined`, и сравнение будет выполнено так, как если бы environment не было вовсе
510
+ - getting autocompletion in IDE,
511
+ - checking the correctness of `env.*` paths,
512
+ - avoiding errors when passing environment.
525
513
 
514
+ > 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.
526
515
 
516
+ ## TypeScript Type Generator
527
517
 
528
- ## Генератор типов для TypeScript
518
+ `AbilityParser.generateTypeDefs()` generates TypeScript types based on policies, allowing you to avoid discrepancies between types and data in policies.
529
519
 
530
- `AbilityParser.generateTypeDefs()` генерирует типы для TypeScript на основе политик, что позволяет не беспокоиться о расхождении между типами и данными в политиках.
520
+ **Usage Example**
531
521
 
532
- **Пример использования**
533
-
534
- Сначала необходимо подготовить массив политик. Политики можно хранить в DSL или в JSON и парсить их в массив готовых политик. В данном примере, для наглядности, политики хранятся в DSL.
522
+ First, you need to prepare an array of policies. Policies can be stored in DSL or JSON and parsed into an array of ready-made policies. In this example, for clarity, policies are stored in DSL.
535
523
 
536
524
  ```ts
537
525
  // scripts/policies.ts
@@ -564,7 +552,7 @@ const typedefs = AbilityParser.generateTypeDefs(policies);
564
552
  writeFileSync('./src/ability/types.generated.ts', typedefs, 'utf8');
565
553
  ```
566
554
 
567
- **Сгенерированный файл (пример)**
555
+ **Generated File (example)**
568
556
 
569
557
  ```ts
570
558
  // src/ability/types.generated.ts
@@ -583,7 +571,7 @@ export type Resources = {
583
571
  };
584
572
  ```
585
573
 
586
- **Использование в коде**
574
+ **Usage in code**
587
575
 
588
576
  ```ts
589
577
  import { AbilityResolver, AbilityPolicy } from '@via-profit/ability';
@@ -599,19 +587,19 @@ await resolver.enforce('order.update', {
599
587
  });
600
588
  ```
601
589
 
602
- ## Отладка политик
590
+ ## Policy Debugging
603
591
 
604
- ### Объяснения
592
+ ### Explanations
605
593
 
606
- Для упрощения отладки политик применяется специальный класс `AbilityResult`, который уже включён в итоговый результат вычислений. `AbilityResult` инкапсулирует итог применения всех подходящих политик к ключу разрешений и ресурсу.
594
+ 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.
607
595
 
608
- `AbilityResult` содержит:
596
+ `AbilityResult` contains:
609
597
 
610
- - список проверенных политик,
611
- - методы для определения итогового эффекта,
612
- - методы для получения объяснений в текстовом представлении.
598
+ - a list of evaluated policies,
599
+ - methods to determine the final effect,
600
+ - methods to get explanations in textual representation.
613
601
 
614
- Пример:
602
+ Example:
615
603
 
616
604
  ```ts
617
605
  const result = await resolver.resolve('order.update', resource);
@@ -627,14 +615,14 @@ const explanations = result.explain(); // AbilityExplain
627
615
 
628
616
  ### AbilityExplain
629
617
 
630
- `AbilityExplain` и связанные классы (`AbilityExplainPolicy`, `AbilityExplainRuleSet`, `AbilityExplainRule`) позволяют получить человекочитаемое объяснение:
618
+ `AbilityExplain` and related classes (`AbilityExplainPolicy`, `AbilityExplainRuleSet`, `AbilityExplainRule`) allow you to get a human-readable explanation:
631
619
 
632
- - какая политика сработала,
633
- - какие группы правил совпали,
634
- - какие правила не прошли,
635
- - какой эффект был применён.
620
+ - which policy matched,
621
+ - which rule groups matched,
622
+ - which rules did not pass,
623
+ - which effect was applied.
636
624
 
637
- Пример использования:
625
+ Usage example:
638
626
 
639
627
  ```ts
640
628
  const result = await resolver.resolve('order.update', resource);
@@ -643,31 +631,30 @@ const explanations = result.explain();
643
631
  console.log(explanations.toString());
644
632
  ```
645
633
 
646
- Пример вывода:
634
+ Example output:
647
635
 
648
636
  ```
649
- ✓ policy «Запрет обновления заказа для менеджеров» is match
650
- ✓ ruleSet «Менеджеры» is match
651
- ✓ rule «Отдел managers» is match
652
- ✗ rule «Роль manager» is mismatch
653
- ✓ ruleSet «Не администраторы» is match
654
- ✓ rule «Нет роли administrator» is match
637
+ ✓ policy «Deny order update for managers» is match
638
+ ✓ ruleSet «Managers» is match
639
+ ✓ rule «Department managers» is match
640
+ ✗ rule «Role manager» is mismatch
641
+ ✓ ruleSet «Not administrators» is match
642
+ ✓ rule «No role administrator» is match
655
643
  ```
656
644
 
657
- ### Формат вывода
658
-
659
- В настоящий момент поддерживается только один формат вывода - текстовый.
645
+ ### Output Format
660
646
 
661
- Вывод строится по принципу: <policy | ruleSet | rule > <название> <is match | is mismatch>
647
+ Currently, only one output format is supported textual.
662
648
 
649
+ The output follows the principle: `<policy | ruleSet | rule> <name> <is match | is mismatch>`
663
650
 
664
- ## Решение проблем
651
+ ## Troubleshooting
665
652
 
666
- ### Модель принятия решений (Default Deny)
653
+ ### Decision‑Making Model (Default Deny)
667
654
 
668
- > Почему политика `deny` не превращается в `permit`, если её условия не выполнены?
655
+ > Why does a `deny` policy not turn into `permit` if its conditions are not met?
669
656
 
670
- Рассмотрим политику, которая **запрещает** доступ пользователю с возрастом 16 лет:
657
+ Consider a policy that **denies** access to a user aged 16:
671
658
 
672
659
  ```ts
673
660
  const dsl = `
@@ -686,10 +673,10 @@ console.log(result.isDenied()); // true ✔
686
673
  console.log(result.isAllowed()); // false ✔
687
674
  ```
688
675
 
689
- В этом случае всё очевидно:
690
- условие выполненополитика совпалаэффект `deny` → доступ запрещён.
676
+ In this case, everything is obvious:
677
+ the condition is met the policy matches effect `deny` → access denied.
691
678
 
692
- **Что происходит, если условия `не выполнены`?**
679
+ **What happens if the conditions are *not met*?**
693
680
 
694
681
  ```ts
695
682
  const result = await resolver.resolve('test', {
@@ -700,69 +687,68 @@ console.log(result.isDenied()); // true ✔
700
687
  console.log(result.isAllowed()); // false ✔
701
688
  ```
702
689
 
703
- На первый взгляд может показаться, что если условие не выполнено, то политика должна «разрешить» доступ.
704
- Но это **не так**.
690
+ At first glance, it might seem that if the condition is not met, the policy should “allow” access.
691
+ But that is **not the case**.
705
692
 
706
- **Модель принятия решений: `Default Deny`**
693
+ **Decision‑Making Model: `Default Deny`**
707
694
 
708
- `AbilityResolver` использует классическую модель безопасности:
695
+ `AbilityResolver` uses the classic security model:
709
696
 
710
- > **Если нет ни одной совпавшей permit‑политикидоступ запрещён.**
697
+ > **If there is no matching permit‑policyaccess is denied.**
711
698
 
712
- **Что происходит в данном примере:**
699
+ **What happens in this example:**
713
700
 
714
- 1. Политика `deny` существует, но её условие **не выполнено**
715
- политика получает статус `mismatch`.
701
+ 1. The `deny` policy exists, but its condition is **not met**
702
+ the policy gets status `mismatch`.
716
703
 
717
- 2. Политика `deny` **не применяется**, потому что условия не совпали.
704
+ 2. The `deny` policy **is not applied** because the conditions did not match.
718
705
 
719
- 3. Политики `permit` **нет**.
706
+ 3. There is no `permit` policy.
720
707
 
721
- 4. Раз нет ни одной разрешающей политикиитоговое решение:
722
- **deny (по умолчанию)**.
708
+ 4. Since there is no permit policythe final decision:
709
+ **deny (by default)**.
723
710
 
711
+ **Summary**
724
712
 
725
- **Итог**
713
+ - `deny` with matching conditions → **deny**
714
+ - `deny` with non‑matching conditions → **deny (default deny)**
715
+ - `permit` with matching conditions → **allow**
716
+ - `permit` with non‑matching conditions → **deny (default deny)**
726
717
 
727
- - `deny` с совпавшими условиями → **deny**
728
- - `deny` с несовпавшими условиями → **deny (default deny)**
729
- - `permit` с совпавшими условиями → **allow**
730
- - `permit` с несовпавшими условиями → **deny (default deny)**
718
+ **Conclusion**
731
719
 
732
- **Заключение**
720
+ **Access is allowed only if there is an explicit permit.**
733
721
 
734
- **Доступ разрешается только при наличии явного permit.**
722
+ ## Design Recommendations
735
723
 
736
- ## Рекомендации по проектированию
724
+ ### Naming Access Keys
737
725
 
738
- ### Именование ключей доступа
726
+ - Use hierarchical keys: `permission.order.create`, `permission.order.update.status`, `permission.user.profile.update`.
727
+ - Group by domains: `permission.user.*`, `permission.order.*`, `permission.product.*`.
728
+ - Do not mix different domains in one key.
739
729
 
740
- - Используйте иерархические ключи: `permission.order.create`, `permission.order.update.status`, `permission.user.profile.update`.
741
- - Группируйте по доменам: `permission.user.*`, `permission.order.*`, `permission.product.*`.
742
- - Не смешивайте разные домены в одном ключе.
730
+ ### Data Structure
743
731
 
744
- ### Структура данных
732
+ - Explicitly describe `Resources` in TypeScript.
733
+ - Do not pass “extra” fields — this complicates understanding.
734
+ - Strive to keep the data structure for a given `permission` stable.
745
735
 
746
- - Явно описывайте `Resources` в TypeScript.
747
- - Не передавайте «лишние» поля — это усложняет понимание.
748
- - Старайтесь, чтобы структура данных для одного `permission` была стабильной.
736
+ ### Policy Design
749
737
 
750
- ### Проектирование политик
738
+ - General rules — via wildcard (`permission.order.*`).
739
+ - Specific restrictions — via exact actions (`permission.order.update`).
740
+ - Use `effect: deny` for prohibitions.
741
+ - Use `effect: permit` for permissions.
751
742
 
752
- - Общие правила — через wildcard (`permission.order.*`).
753
- - Специфичные ограничения — через точные действия (`permission.order.update`).
754
- - Для запретов используйте `effect: deny`.
755
- - Для разрешений — `effect: permit`.
743
+ ### Common Mistakes
756
744
 
757
- ### Типичные ошибки
745
+ - Expecting that absence of matching policies means allow.
746
+ - Mixing business logic and access policies.
747
+ - Too large policies with dozens of rules — better to break them down.
758
748
 
759
- - Ожидание, что отсутствие совпавших политик означает deny.
760
- - Смешивание бизнес-логики и политик доступа.
761
- - Слишком крупные политики с десятками правил — лучше разбивать.
749
+ ### Example of Use on the Frontend (React)
762
750
 
763
- ### Пример использования на фронтенде (React)
764
-
765
- **Хук для проверки политик**
751
+ **Hook for checking policies**
766
752
 
767
753
  ```tsx
768
754
  // hooks/use-ability.ts
@@ -804,7 +790,7 @@ export function useAbility<Permission extends keyof Resources>(
804
790
  }
805
791
  ```
806
792
 
807
- **Использование в компоненте**
793
+ **Usage in a component**
808
794
 
809
795
  ```tsx
810
796
  function OrderUpdateButton({ order, user }) {
@@ -814,7 +800,7 @@ function OrderUpdateButton({ order, user }) {
814
800
  });
815
801
 
816
802
  if (allowed === null) {
817
- return null; // или бейдж загрузки
803
+ return null; // or loading spinner
818
804
  }
819
805
 
820
806
  if (!allowed) {
@@ -825,79 +811,75 @@ function OrderUpdateButton({ order, user }) {
825
811
  }
826
812
  ```
827
813
 
814
+ ## Examples
828
815
 
829
- ## Примеры
830
-
831
-
832
- ### Пример сложной многоступенчатой политики
833
-
834
- Ниже - многоступенчатый набор политик, на примере использования в кинотеатре (выдуманный пример).
835
-
836
- **Пример демонстрирует:**
837
- - работу с ролями (admin, seller, manager, VIP, banned),
838
- - временн́ые ограничения (`env.time.hour`),
839
- - wildcard‑права (`permission.*`),
840
- - ограничения по количеству билетов,
841
- - запрет на продажу уже проданных билетов,
842
- - комбинацию `permit`/`deny`‑политик,
843
- - приоритет политик и модель Default Deny.
816
+ ### Example of a Complex Multi‑Level Policy
844
817
 
818
+ Below is a multi‑level set of policies, using a cinema example (fictional).
845
819
 
846
- **Краткое описание правил**
847
- - **Администратор**
848
- Имеет wildcard‑права (`permission.*`) и может выполнять любые действия.
849
- Может редактировать стоимость билетов.
820
+ **The example demonstrates:**
821
+ - working with roles (admin, seller, manager, VIP, banned),
822
+ - time constraints (`env.time.hour`),
823
+ - wildcard permissions (`permission.*`),
824
+ - ticket quantity limits,
825
+ - prohibition on selling already sold tickets,
826
+ - combination of `permit`/`deny` policies,
827
+ - policy priority and Default Deny model.
850
828
 
851
- - **Продавец**
852
- Может продавать билеты только в рабочие часы (09:00–23:00).
853
- Не может продавать билеты, если:
854
- - кинотеатр закрыт,
855
- - билет уже продан.
829
+ **Brief description of rules**
830
+ - **Administrator**
831
+ Has wildcard permissions (`permission.*`) and can perform any action.
832
+ Can edit ticket prices.
856
833
 
857
- - **Менеджер**
858
- Имеет те же права, что и продавец.
834
+ - **Seller**
835
+ Can sell tickets only during working hours (09:00–23:00).
836
+ Cannot sell tickets if:
837
+ - the cinema is closed,
838
+ - the ticket is already sold.
859
839
 
860
- - **Покупатели**
861
- - Пользователь старше 21 года может покупать билеты.
862
- - VIP‑пользователь может покупать билеты в любое время.
863
- - Заблокированный пользователь (`status = banned`) не может покупать билеты.
864
- - Любой пользователь не может купить более 6 билетов.
840
+ - **Manager**
841
+ Has the same rights as a seller.
865
842
 
843
+ - **Buyers**
844
+ - A user older than 21 can buy tickets.
845
+ - A VIP user can buy tickets at any time.
846
+ - A banned user (`status = banned`) cannot buy tickets.
847
+ - Any user cannot buy more than 6 tickets.
866
848
 
867
- **Общая диаграмма политик**
849
+ **Policy Diagram**
868
850
 
869
851
  ```mermaid
870
852
  flowchart LR
871
853
 
872
854
  %% ==== ROLES ====
873
855
 
874
- subgraph Roles[Роли]
875
- A[Администратор]
876
- B[Продавец]
877
- C[Менеджер]
856
+ subgraph Roles[Roles]
857
+ A[Administrator]
858
+ B[Seller]
859
+ C[Manager]
878
860
  end
879
861
 
880
- subgraph Buyers[Покупатели]
881
- U1[Пользователь > 21]
882
- U2[VIP пользователь]
883
- U3[Заблокированный пользователь]
862
+ subgraph Buyers[Buyers]
863
+ U1[User > 21]
864
+ U2[VIP user]
865
+ U3[Banned user]
884
866
  end
885
867
 
886
868
  %% ==== ADMIN ====
887
869
 
888
870
  A --> A1[Wildcard: permission.*]
889
- A --> A2[Редактировать цену билетов]
871
+ A --> A2[Edit ticket price]
890
872
 
891
- A1 --> FINAL[Итоговое решение]
873
+ A1 --> FINAL[Final decision]
892
874
  A2 --> FINAL
893
875
 
894
876
  %% ==== SELLER ====
895
877
 
896
- B --> B1[Продавать билеты]
878
+ B --> B1[Sell tickets]
897
879
 
898
- B1 -->|09:00–23:00| B2[Разрешено]
899
- B1 -->|Вне времени| D2[Запрещено]
900
- B1 -->|ticket.status = sold| D3[Запрещено]
880
+ B1 -->|09:00–23:00| B2[Allowed]
881
+ B1 -->|Outside hours| D2[Denied]
882
+ B1 -->|ticket.status = sold| D3[Denied]
901
883
 
902
884
  B2 --> FINAL
903
885
  D2 --> FINAL
@@ -905,20 +887,20 @@ flowchart LR
905
887
 
906
888
  %% ==== MANAGER ====
907
889
 
908
- C --> C1[Продавать билеты как продавец]
890
+ C --> C1[Sell tickets as seller]
909
891
  C1 --> FINAL
910
892
 
911
893
  %% ==== BUYERS ====
912
894
 
913
- U1 --> U1A[Покупать билеты]
914
- U1A -->|ticketsCount < 6| U1OK[Разрешено]
915
- U1A -->|ticketsCount ≥ 6| U1DENY[Запрещено]
895
+ U1 --> U1A[Buy tickets]
896
+ U1A -->|ticketsCount < 6| U1OK[Allowed]
897
+ U1A -->|ticketsCount ≥ 6| U1DENY[Denied]
916
898
 
917
- U2 --> U2A[Покупать билеты в любое время]
918
- U2A -->|ticketsCount < 6| U2OK[Разрешено]
919
- U2A -->|ticketsCount ≥ 6| U2DENY[Запрещено]
899
+ U2 --> U2A[Buy tickets anytime]
900
+ U2A -->|ticketsCount < 6| U2OK[Allowed]
901
+ U2A -->|ticketsCount ≥ 6| U2DENY[Denied]
920
902
 
921
- U3 --> U3A[Запрещено покупать билеты]
903
+ U3 --> U3A[Denied to buy tickets]
922
904
 
923
905
  U1OK --> FINAL
924
906
  U1DENY --> FINAL
@@ -928,11 +910,10 @@ flowchart LR
928
910
 
929
911
  %% ==== DENY RULES ====
930
912
 
931
- D1[Запрещено покупать билеты, если user.status = banned] --> FINAL
932
-
913
+ D1[Denied to buy tickets if user.status = banned] --> FINAL
933
914
  ```
934
915
 
935
- **DSL политик**
916
+ **DSL Policies**
936
917
 
937
918
  ```dsl
938
919
  ############################################################
@@ -998,13 +979,11 @@ deny permission.ticket.buy if all:
998
979
  # @name Cannot sell already sold tickets
999
980
  deny permission.ticket.sell if all:
1000
981
  ticket.status is equals 'sold'
1001
-
1002
982
  ```
1003
983
 
984
+ Below is how to use the policies above in Node.js + TypeScript.
1004
985
 
1005
- Ниже показано, как использовать приведённые выше политики в Node.js + TypeScript.
1006
-
1007
- **Подготовка политик**
986
+ **Preparing Policies**
1008
987
 
1009
988
  ```ts
1010
989
  import { AbilityDSLParser } from '@via-profit/ability';
@@ -1013,7 +992,7 @@ import cinemaDSL from './policies/cinema.dsl';
1013
992
  export const policies = new AbilityDSLParser(cinemaDSL).parse();
1014
993
  ```
1015
994
 
1016
- **Создание резолвера**
995
+ **Creating the Resolver**
1017
996
 
1018
997
  ```ts
1019
998
  import { AbilityResolver } from '@via-profit/ability';
@@ -1022,27 +1001,24 @@ import { policies } from './policies';
1022
1001
  const resolver = new AbilityResolver(policies);
1023
1002
  ```
1024
1003
 
1025
- **Проверка разрешений (enforce)**
1004
+ **Checking Permissions (enforce)**
1026
1005
 
1027
- Пример: покупка билета.
1006
+ Example: buying a ticket.
1028
1007
 
1029
- Метод enforce выбрасывает исключение `AbilityError`, если доступ запрещён.
1008
+ The `enforce` method throws an `AbilityError` if access is denied.
1030
1009
 
1031
1010
  ```ts
1032
1011
  await resolver.enforce('ticket.buy', {
1033
1012
  user: { age: 25, ticketsCount: 1 },
1034
1013
  env: { time: { hour: 18 } },
1035
1014
  });
1036
-
1037
1015
  ```
1038
- Если разрешенокод продолжит выполнение.
1039
- Если запрещенобудет выброшено исключение `AbilityError`.
1016
+ If allowedthe code continues execution.
1017
+ If deniedan `AbilityError` exception is thrown.
1040
1018
 
1019
+ **Checking Permissions Without Exceptions (resolve)**
1041
1020
 
1042
-
1043
- **Проверка разрешений без исключений (resolve)**
1044
-
1045
- `resolve` возвращает объект результата:
1021
+ `resolve` returns a result object:
1046
1022
 
1047
1023
  ```ts
1048
1024
  const result = await resolver.resolve('ticket.buy', {
@@ -1051,14 +1027,13 @@ const result = await resolver.resolve('ticket.buy', {
1051
1027
  });
1052
1028
 
1053
1029
  if (result.isAllowed()) {
1054
- console.log('Покупка разрешена');
1030
+ console.log('Purchase allowed');
1055
1031
  } else {
1056
- console.log('Покупка запрещена');
1032
+ console.log('Purchase denied');
1057
1033
  }
1058
-
1059
1034
  ```
1060
1035
 
1061
- **Продавец может продавать только в рабочие часы***
1036
+ **Seller can only sell during working hours**
1062
1037
 
1063
1038
  ```ts
1064
1039
  await resolver.enforce('ticket.sell', {
@@ -1066,12 +1041,11 @@ await resolver.enforce('ticket.sell', {
1066
1041
  env: { time: { hour: 15 } },
1067
1042
  ticket: { status: 'available' },
1068
1043
  });
1069
-
1070
1044
  ```
1071
1045
 
1072
- **Подготовка данных для резолвера**
1046
+ **Preparing Data for the Resolver**
1073
1047
 
1074
- В примерах выше в резолвер передаются простые константные объекты:
1048
+ In the examples above, constant objects are passed to the resolver:
1075
1049
 
1076
1050
  ```ts
1077
1051
  resolver.enforce('ticket.buy', {
@@ -1080,33 +1054,32 @@ resolver.enforce('ticket.buy', {
1080
1054
  });
1081
1055
  ```
1082
1056
 
1083
- Это сделано для наглядности. В реальном приложении данные для резолвера должны формироваться динамическииз тех источников, которые доступны вашему серверу.
1057
+ 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.
1084
1058
 
1085
- **Пользователь** (`user`) обычно берётся из:
1059
+ **User** (`user`) is usually taken from:
1086
1060
 
1061
+ - JWT token
1062
+ - session
1063
+ - database
1064
+ - authorization middleware
1087
1065
 
1088
- - JWT‑токена
1089
- - сессии
1090
- - базы данных
1091
- - middleware авторизации
1092
-
1093
- Пример:
1066
+ Example:
1094
1067
 
1095
1068
  ```ts
1096
1069
  const user = await db.users.findById(session.userId);
1097
1070
  ```
1098
1071
 
1099
- **Окружение (Environment)** (`env`)
1072
+ **Environment** (`env`)
1100
1073
 
1101
- Это любые внешние параметры, которые могут влиять на доступ:
1074
+ These are any external parameters that can affect access:
1102
1075
 
1103
- - текущее время сервера
1104
- - часовой пояс
1105
- - IP‑адрес
1106
- - заголовки запроса
1107
- - конфигурация системы
1076
+ - current server time
1077
+ - time zone
1078
+ - IP address
1079
+ - request headers
1080
+ - system configuration
1108
1081
 
1109
- Пример:
1082
+ Example:
1110
1083
 
1111
1084
  ```ts
1112
1085
  const env = {
@@ -1117,32 +1090,31 @@ const env = {
1117
1090
  };
1118
1091
  ```
1119
1092
 
1120
- **Ресурс** (например, `ticket`)
1093
+ **Resource** (e.g., `ticket`)
1121
1094
 
1122
- Если действие связано с конкретным объектом его тоже нужно загрузить:
1095
+ If the action is associated with a specific object, it also needs to be loaded:
1123
1096
 
1124
1097
  ```ts
1125
1098
  const ticket = await db.tickets.findById(req.params.ticketId);
1126
1099
  ```
1127
1100
 
1128
- **Контекст**
1129
-
1130
- Контекст — это объект, который вы передаёте в `resolve` или `enforce`.
1131
- Он содержит **все данные**, которые могут понадобиться политикам:
1101
+ **Context**
1132
1102
 
1133
- - `user` данные о текущем пользователе
1134
- - `env` данные окружения (время, IP, география, настройки системы)
1135
- - `resource` или `ticket` — данные о сущности, над которой выполняется действие
1136
- - любые другие объекты, которые вы используете в DSL
1103
+ Context is the object that you pass to `resolve` or `enforce`.
1104
+ It contains **all the data** that policies might need:
1137
1105
 
1138
- **Важно понимать:**
1106
+ - `user` — data about the current user
1107
+ - `env` — environment data (time, IP, geography, system settings)
1108
+ - `resource` or `ticket` — data about the entity on which the action is performed
1109
+ - any other objects that you use in DSL
1139
1110
 
1140
- > Контекст формируется под конкретное действие и конкретные политики. Его не нужно хранить заранее — вы собираете его динамически перед вызовом резолвера.
1111
+ **It is important to understand:**
1141
1112
 
1113
+ > 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.
1142
1114
 
1143
- ## Производительность
1115
+ ## Performance
1144
1116
 
1145
- В тестах задействовались политики на 10 условий, вложенные поля, environment.
1117
+ The tests used policies with 10 conditions, nested fields, and environment.
1146
1118
 
1147
1119
  **Tinybench** ([https://github.com/tinylibs/tinybench](https://github.com/tinylibs/tinybench))
1148
1120
 
@@ -1168,10 +1140,8 @@ Throughput (ops/s)
1168
1140
  1580 | █████████████████████████████████████ resolve() — no cache
1169
1141
  --------------------------------------------------------------
1170
1142
  no cache cold cache warm cache
1171
-
1172
1143
  ```
1173
1144
 
1145
+ ## License
1174
1146
 
1175
- ## Лицензия
1176
-
1177
- Этот проект лицензирован под лицензией MIT. Подробности в файле [LICENSE](LICENSE).
1147
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.