@via-profit/ability 3.2.0 → 3.4.0

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/CHANGELOG.md CHANGED
@@ -1,129 +1,2 @@
1
1
  # Changelog
2
2
 
3
- ## [3.2.0] - 2026-x3-xx
4
-
5
- ### Изменено
6
-
7
- - Метод `AbilityPolicy.parseAll()` переименован в `AbilityPolicy.fromJSONAll()`
8
- - Метод `AbilityPolicy.parse()` переименован в `AbilityPolicy.toJSON()`
9
- - Метод `AbilityRule.parse()` переименован в `AbilityRule.toJSON()`
10
- - Метод `AbilityRuleSet.parse()` переименован в `.toJSON()`
11
-
12
- ## [3.1.0] - 2026-03-20
13
-
14
- ### Добавлено
15
-
16
- - Реализован кэш
17
- - Добавлен кэш-провайдер `AbilityCacheProvider` для реализации кастомного кэша
18
- - Реализован и включён по умолчанию `AbilityInMemoryCache` (кэш в памяти)
19
- - Добавлена полноценная поддержка `environment` как третьего аргумента в:
20
- - `resolver.resolve(action, resource, environment)`
21
- - `resolver.enforce(action, resource, environment)`
22
- - Введена возможность использовать пути вида `env.*` в правилах политик.
23
- - Пример: `"subject": "env.time.hour"`
24
- - Добавлена поддержка смешанных сравнений:
25
- - `resource.*` ↔ `env.*`
26
- - литерал ↔ `env.*`
27
- - `env.*` ↔ литерал
28
-
29
- ### Breaking changes
30
-
31
- Асинхронизация механизма проверки политик.
32
- Все методы, участвующие в цепочке вычисления разрешений, теперь возвращают `Promise`.
33
-
34
- #### Изменено
35
-
36
- - `AbilityRule.check(resource): Promise<AbilityMatch>`
37
- Ранее возвращал `AbilityMatch` синхронно.
38
-
39
- - `AbilityRuleSet.check(resource): Promise<AbilityMatch>`
40
- Теперь выполняет правила последовательно и асинхронно.
41
-
42
- - `AbilityPolicy.check(resource): Promise<AbilityMatch>`
43
- Асинхронно проверяет ruleSet в строгом порядке.
44
-
45
- - `AbilityResolver.resolve(action, resource): Promise<AbilityResult>`
46
- Теперь асинхронный метод, который дожидается выполнения всех политик.
47
-
48
- - `AbilityResolver.enforce(action, resource): Promise<void | never>`
49
- Теперь работает асинхронно.
50
-
51
- ### Миграция
52
-
53
- 1. Все вызовы `check()` должны быть обновлены:
54
-
55
- ```ts
56
- await rule.check(resource);
57
- await ruleSet.check(resource);
58
- await policy.check(resource);
59
- ```
60
-
61
- 2. Все вызовы `resolver.resolve()` и `resolver.enforce()` теперь требуют `await`:
62
-
63
- ```ts
64
- await resolver.resolve('order.update', resource);
65
- await resolver.enforce('order.update', resource);
66
- ```
67
-
68
- ## [3.0.1] - 2026-03-19
69
-
70
- ## Добавлено
71
-
72
- ### 1. **Лицензия MIT** (`LICENSE`)
73
-
74
- - Добавлен официальный файл лицензии MIT от Via Profit
75
-
76
- ### 2. **Класс `AbilityExplain.ts`**
77
-
78
- - Новый класс для получения человекочитаемых объяснений результатов проверки
79
- - Классы-наследники:
80
- - `AbilityExplainRule` - объяснение для правила
81
- - `AbilityExplainRuleSet` - объяснение для группы правил
82
- - `AbilityExplainPolicy` - объяснение для политики
83
- - Метод `toString()` форматирует вывод с отступами и символами ✓/✗
84
-
85
- ---
86
-
87
- ## Обновлено
88
-
89
- ### **AbilityParser.ts** (полная переработка)
90
-
91
- - **Было**: Базовая генерация типов
92
- - **Стало**: Расширенная система генерации TypeScript типов
93
- - Новые методы:
94
- - `determineTypeFromRule()` - определение типа на основе правила
95
- - `getArrayType()` - обработка массивов
96
- - `getPrimitiveType()` - определение примитивных типов
97
- - `buildNestedStructure()` - трансформация плоской структуры во вложенную
98
- - `formatTypeDefinitions()` - форматирование финального вывода
99
- - `formatNestedObject()` - рекурсивное форматирование объектов
100
-
101
- ### **AbilityRule.ts**
102
-
103
- - `id` и `name` теперь опциональные (`?`)
104
- - Автогенерация `id` и `name` если не предоставлены
105
- - **Новые статические методы** (фабричные методы):
106
- - `equal()`, `notEqual()`, `in()`, `notIn()`
107
- - `lessThan()`, `lessOrEqual()`, `moreThan()`, `moreOrEqual()`
108
-
109
- ### **AbilityRuleSet.ts**
110
-
111
- - `id` и `name` теперь опциональные
112
- - Добавлены статические методы:
113
- - `and()` - создание группы с логическим И
114
- - `or()` - создание группы с логическим ИЛИ
115
-
116
- ### **AbilityPolicy.ts**
117
-
118
- - Новый метод `explain()` - получение объяснения проверки
119
- - Новый статический метод `parseAll()` - парсинг массива конфигураций
120
- - Улучшены комментарии к полю `action`
121
-
122
- ### **AbilityResolver.ts**
123
-
124
- - Новый метод `resolveWithExplain()` - проверка с детальным объяснением
125
- - Возвращает массив `AbilityExplain[]` для анализа результатов
126
-
127
- ---
128
-
129
- ## Обновлена документация и примеры
package/README.md CHANGED
@@ -3,6 +3,15 @@
3
3
  > A set of services that partially implement the [Attribute Based Access Control](https://en.wikipedia.org/wiki/Attribute-based_access_control) principle.
4
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
+ ![npm version](https://img.shields.io/npm/v/%40via-profit/ability)
7
+ ![npm downloads](https://img.shields.io/npm/dm/%40via-profit/ability)
8
+ ![license](https://img.shields.io/github/license/via-profit/ability)
9
+ ![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue)
10
+ ![status](https://img.shields.io/badge/status-active-success)
11
+ ![issues](https://img.shields.io/github/issues/via-profit/ability)
12
+ ![stars](https://img.shields.io/github/stars/via-profit/ability?style=social)
13
+
14
+
6
15
  ## Language / Язык
7
16
 
8
17
  - [🇬🇧 English](/docs/en/README.md)
@@ -543,6 +552,34 @@ order.total
543
552
  | **length greater than** | `len >` | `tags length greater than 2` | Length greater than | array, string |
544
553
  | **length less than** | `len <` | `tags length less than 5` | Length less than | array, string |
545
554
 
555
+ Here is the English version, keeping the structure and tone consistent with your documentation style.
556
+
557
+ **Special Operators**
558
+
559
+ | DSL Operator | Synonyms | Example | Description | Types |
560
+ |--------------|----------|---------|-------------|--------|
561
+ | **always** | — | `always` | The condition is always true. Used for global allow rules or simplifying logic. | special operator |
562
+ | **never** | — | `never` | The condition is always false. Used for global deny rules or disabling a rule. | special operator |
563
+
564
+
565
+ **always**
566
+ An operator that always returns `true`.
567
+ Used for:
568
+
569
+ - global allow (`permit permission.* if all: always`)
570
+ - testing
571
+ - disabling complex conditions
572
+ - creating fallback rules
573
+
574
+ **never**
575
+ An operator that always returns `false`.
576
+ Used for:
577
+
578
+ - global deny (`deny permission.* if all: never`)
579
+ - temporarily disabling a rule
580
+ - explicit unconditional rejection
581
+
582
+
546
583
  #### Value
547
584
 
548
585
  Supported values:
@@ -693,18 +730,14 @@ This allows:
693
730
 
694
731
  ## TypeScript Type Generator
695
732
 
696
- `AbilityParser.generateTypeDefs()` generates TypeScript types based on policies, allowing you to avoid discrepancies between types and data in policies.
733
+ `AbilityTypeGenerator.generateTypeDefs(policies)` generates TypeScript types based on policies, allowing you to avoid inconsistencies between types and the data in the policies.
697
734
 
698
- **Usage Example**
735
+ **Example usage**
699
736
 
700
- 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.
737
+ Policies can be stored in DSL or JSON. This example uses a DSL file.
701
738
 
702
- ```ts
703
- // scripts/policies.ts
704
-
705
- import { AbilityDSLParser } from '@via-profit/ability';
706
-
707
- const dsl = `
739
+ _policies/policies.dsl_
740
+ ```
708
741
  # @name Update order
709
742
  permit permission.order.update if all:
710
743
 
@@ -712,25 +745,46 @@ permit permission.order.update if all:
712
745
  all of:
713
746
  # @name User is owner
714
747
  user.id = order.ownerId
715
- `;
748
+ ```
749
+
750
+ _scripts/policies.js_
751
+ ```js
752
+ const fs = require('node:fs');
753
+ const path = require('node:path');
754
+ const { AbilityTypeGenerator, AbilityDSLParser } = require('@via-profit/ability');
755
+
756
+ // Prepare paths
757
+ const dslPath = path.resolve(__dirname, '../src/policies/policies.dsl');
758
+ const typeDefsPath = path.join(path.dirname(dslPath), 'policies.types.ts');
716
759
 
760
+ // Read DSL as a string
761
+ const dsl = fs.readFileSync(dslPath, {encoding: 'utf-8'});
762
+
763
+ // Create policies
717
764
  const policies = new AbilityDSLParser(dsl).parse();
718
765
 
719
- export default policies;
766
+ // Generate TypeScript types
767
+ const typeDefs = new AbilityTypeGenerator(policies).generateTypeDefs();
768
+
769
+ // Save TypeScript types to file
770
+ fs.writeFileSync(typeDefsPath, typeDefs, {encoding: 'utf-8'});
720
771
  ```
721
772
 
773
+ _policies/index.ts_
722
774
  ```ts
723
- // scripts/generate-types.ts
724
- import { writeFileSync } from 'node:fs';
725
- import { AbilityParser } from '@via-profit/ability';
726
- import policies from './policies.json';
775
+ import { AbilityDSLParser, AbilityResolver } from '@via-profit/ability';
776
+ import type { Resources } from './policies.types';
777
+ import dsl from './policies.dsl';
727
778
 
728
- const typedefs = AbilityParser.generateTypeDefs(policies);
779
+ const policies = new AbilityDSLParser<Resources>(dsl).parse();
780
+
781
+ export const policyResolver = new AbilityResolver(new AbilityDSLParser<Resources>(dsl).parse());
782
+
783
+ export default policyResolver;
729
784
 
730
- writeFileSync('./src/ability/types.generated.ts', typedefs, 'utf8');
731
785
  ```
732
786
 
733
- **Generated File (example)**
787
+ **Generated file (example)**
734
788
 
735
789
  ```ts
736
790
  // src/ability/types.generated.ts
@@ -752,12 +806,7 @@ export type Resources = {
752
806
  **Usage in code**
753
807
 
754
808
  ```ts
755
- import { AbilityResolver, AbilityPolicy } from '@via-profit/ability';
756
- import type { Resources } from './ability/types.generated';
757
-
758
- const resolver = new AbilityResolver<Resources>(
759
- AbilityPolicy.parseAll(policies),
760
- );
809
+ import { policyResolver } from './policies';
761
810
 
762
811
  await resolver.enforce('order.update', {
763
812
  user: { id: 'u1' },
@@ -1322,4 +1371,4 @@ Throughput (ops/s)
1322
1371
 
1323
1372
  ## License
1324
1373
 
1325
- This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
1374
+ This project is licensed under the MIT License. See the [LICENSE](/LICENSE) file for details.
@@ -1,6 +1,6 @@
1
1
  import AbilityCode from './AbilityCode';
2
- export type AbilityConditionCodeType = '=' | '<>' | '>' | '<' | '>=' | '<=' | 'in' | 'not in' | 'contains' | 'not contains' | 'length greater than' | 'length less than' | 'length equals';
3
- export type AbilityConditionLiteralType = 'equals' | 'not_equals' | 'contains' | 'no_contains' | 'in' | 'not_in' | 'greater_than' | 'less_than' | 'less_or_equal' | 'greater_or_equal' | 'length_greater_than' | 'length_less_than' | 'length_equals';
2
+ export type AbilityConditionCodeType = '=' | '<>' | '>' | '<' | '>=' | '<=' | 'in' | 'not in' | 'contains' | 'not contains' | 'length greater than' | 'length less than' | 'length equals' | 'always' | 'never';
3
+ export type AbilityConditionLiteralType = 'equals' | 'not_equals' | 'contains' | 'no_contains' | 'in' | 'not_in' | 'greater_than' | 'less_than' | 'less_or_equal' | 'greater_or_equal' | 'length_greater_than' | 'length_less_than' | 'length_equals' | 'always' | 'never';
4
4
  export declare class AbilityCondition extends AbilityCode<AbilityConditionCodeType> {
5
5
  static equals: AbilityCondition;
6
6
  static not_equals: AbilityCondition;
@@ -15,6 +15,8 @@ export declare class AbilityCondition extends AbilityCode<AbilityConditionCodeTy
15
15
  static length_greater_than: AbilityCondition;
16
16
  static length_less_than: AbilityCondition;
17
17
  static length_equals: AbilityCondition;
18
+ static always: AbilityCondition;
19
+ static never: AbilityCondition;
18
20
  static fromLiteral(literal: AbilityConditionLiteralType): AbilityCondition;
19
21
  get literal(): AbilityConditionLiteralType;
20
22
  }
@@ -3,7 +3,7 @@ import AbilityMatch from './AbilityMatch';
3
3
  import AbilityCompare, { AbilityCompareCodeType } from './AbilityCompare';
4
4
  import AbilityPolicyEffect, { AbilityPolicyEffectCodeType } from './AbilityPolicyEffect';
5
5
  import { AbilityExplain } from './AbilityExplain';
6
- import { ResourceObject } from './AbilityParser';
6
+ import { ResourceObject } from './AbilityTypeGenerator';
7
7
  export type AbilityPolicyConfig = {
8
8
  readonly permission: string;
9
9
  readonly effect: AbilityPolicyEffectCodeType;
@@ -1,6 +1,6 @@
1
1
  import AbilityPolicy from './AbilityPolicy';
2
2
  import { AbilityResult } from './AbilityResult';
3
- import { ResourcesMap } from './AbilityParser';
3
+ import { ResourcesMap } from './AbilityTypeGenerator';
4
4
  import { AbilityCacheAdapter } from '../cache/AbilityCacheAdapter';
5
5
  export type AbilityResolverOptions = {
6
6
  readonly cache?: AbilityCacheAdapter | null;
@@ -1,5 +1,5 @@
1
1
  import { AbilityExplain } from './AbilityExplain';
2
- import { ResourceObject } from './AbilityParser';
2
+ import { ResourceObject } from './AbilityTypeGenerator';
3
3
  import AbilityPolicy from './AbilityPolicy';
4
4
  import AbilityPolicyEffect from './AbilityPolicyEffect';
5
5
  export declare class AbilityResult<Resource extends ResourceObject = Record<string, unknown>> {
@@ -1,7 +1,7 @@
1
1
  import AbilityRule, { AbilityRuleConfig } from './AbilityRule';
2
2
  import AbilityCompare, { AbilityCompareCodeType } from './AbilityCompare';
3
3
  import AbilityMatch from './AbilityMatch';
4
- import { ResourceObject } from './AbilityParser';
4
+ import { ResourceObject } from './AbilityTypeGenerator';
5
5
  export type AbilityRuleSetConfig = {
6
6
  readonly id?: string | null;
7
7
  readonly name?: string | null;
@@ -0,0 +1,55 @@
1
+ import AbilityPolicy from './AbilityPolicy';
2
+ export type Primitive = string | number | boolean | null | undefined;
3
+ export type NestedDict<T = Primitive> = {
4
+ [key: string]: NestedDict<T> | T;
5
+ };
6
+ export type ResourceObject = Record<string, unknown>;
7
+ export type ResourcesMap = Record<string, ResourceObject>;
8
+ export declare class AbilityTypeGenerator {
9
+ readonly policies: readonly AbilityPolicy[];
10
+ constructor(policies: readonly AbilityPolicy[]);
11
+ /**
12
+ * Generates TypeScript type definitions based on the provided policies.
13
+ * @returns A generated type definitions.
14
+ */
15
+ generateTypeDefs(): string;
16
+ /**
17
+ * Determines TypeScript type based on the rule
18
+ * @param rule - The rule to analyze
19
+ * @returns TypeScript type as string
20
+ */
21
+ private determineTypeFromRule;
22
+ /**
23
+ * Gets TypeScript type for array values
24
+ * @param resource - The resource value to analyze
25
+ * @returns TypeScript array type as string
26
+ */
27
+ private getArrayType;
28
+ /**
29
+ * Gets primitive TypeScript type for a value
30
+ * @param value - The value to analyze
31
+ * @returns TypeScript primitive type as string
32
+ */
33
+ private getPrimitiveType;
34
+ /**
35
+ * Builds nested structure from flat paths
36
+ * Example: 'user.profile.name' -> { user: { profile: { name: 'string' } } }
37
+ * @param flatStructure - Flat structure with dot notation paths
38
+ * @returns Nested object structure
39
+ */
40
+ private buildNestedStructure;
41
+ /**
42
+ * Formats type structure into a string
43
+ * @param structure - Nested type structure
44
+ * @returns Formatted TypeScript type definition string
45
+ */
46
+ private formatTypeDefinitions;
47
+ /**
48
+ * Recursively formats nested object
49
+ * @param obj - Object to format
50
+ * @param indent - Current indentation level
51
+ * @returns Formatted string
52
+ */
53
+ private formatNestedObject;
54
+ }
55
+ export default AbilityTypeGenerator;
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export * from './core/AbilityCompare';
3
3
  export * from './core/AbilityCondition';
4
4
  export * from './core/AbilityError';
5
5
  export * from './core/AbilityMatch';
6
- export * from './core/AbilityParser';
6
+ export * from './core/AbilityTypeGenerator';
7
7
  export * from './core/AbilityPolicy';
8
8
  export * from './core/AbilityPolicyEffect';
9
9
  export * from './core/AbilityResolver';
package/dist/index.js CHANGED
@@ -46,6 +46,8 @@ class AbilityCondition extends AbilityCode {
46
46
  static length_greater_than = new AbilityCondition('length greater than');
47
47
  static length_less_than = new AbilityCondition('length less than');
48
48
  static length_equals = new AbilityCondition('length equals');
49
+ static always = new AbilityCondition('always');
50
+ static never = new AbilityCondition('never');
49
51
  static fromLiteral(literal) {
50
52
  switch (literal) {
51
53
  case 'equals':
@@ -72,6 +74,10 @@ class AbilityCondition extends AbilityCode {
72
74
  return this.length_greater_than;
73
75
  case 'length_equals':
74
76
  return this.length_equals;
77
+ case 'always':
78
+ return this.always;
79
+ case 'never':
80
+ return this.never;
75
81
  default:
76
82
  throw new AbilityParserError(`Literal ${literal} does not found in AbilityCondition class`);
77
83
  }
@@ -94,56 +100,20 @@ class AbilityMatch extends AbilityCode {
94
100
  static mismatch = new AbilityMatch('mismatch');
95
101
  }
96
102
 
97
- class AbilityParser {
98
- /**
99
- * Sets a value in a nested object structure based on a dot/bracket notation path.
100
- * @param object - The target object to modify.
101
- * @param path - The path to the property in dot/bracket notation.
102
- * @param value - The value to set at the specified path.
103
- */
104
- static setValueDotValue(object, path, value) {
105
- if (!path || path.trim().length === 0) {
106
- throw new AbilityParserError(`Invalid path provided on a [${path}]`);
107
- }
108
- const way = path.replace(/\[/g, '.').replace(/]/g, '').split('.');
109
- const last = way.pop();
110
- if (!last) {
111
- throw new AbilityParserError(`Invalid path provided on a [${path}]`);
112
- }
113
- const lastObj = way.reduce((acc, key, index, array) => {
114
- const currentValue = acc[key];
115
- const nextKey = array[index + 1];
116
- const shouldBeArray = nextKey !== undefined && isFinite(Number(nextKey));
117
- if (currentValue === undefined || currentValue === null) {
118
- // Create missing property
119
- const newValue = shouldBeArray ? [] : {};
120
- acc[key] = newValue;
121
- return newValue;
122
- }
123
- if (typeof currentValue !== 'object') {
124
- throw new AbilityParserError(`Cannot set property '${key}' on non-object value at path: ${path}`);
125
- }
126
- return currentValue;
127
- }, object);
128
- const existingValue = lastObj[last];
129
- if (existingValue !== undefined &&
130
- typeof existingValue === 'object' &&
131
- existingValue !== null &&
132
- !Array.isArray(existingValue)) {
133
- throw new AbilityParserError(`Cannot set primitive value on existing object at path: ${path}`);
134
- }
135
- lastObj[last] = value;
103
+ class AbilityTypeGenerator {
104
+ policies;
105
+ constructor(policies) {
106
+ this.policies = policies;
136
107
  }
137
108
  /**
138
109
  * Generates TypeScript type definitions based on the provided policies.
139
- * @param policies - An array of AbilityPolicy instances.
140
110
  * @returns A generated type definitions.
141
111
  */
142
- static generateTypeDefs(policies) {
112
+ generateTypeDefs() {
143
113
  // Structure to store types: { [action]: { [subjectPath]: type } }
144
114
  const typeStructure = {};
145
115
  // Iterate through all policies
146
- policies.forEach(policy => {
116
+ this.policies.forEach(policy => {
147
117
  const action = policy.permission;
148
118
  // Initialize object for action if it doesn't exist
149
119
  if (!typeStructure[action]) {
@@ -175,7 +145,7 @@ class AbilityParser {
175
145
  * @param rule - The rule to analyze
176
146
  * @returns TypeScript type as string
177
147
  */
178
- static determineTypeFromRule(rule) {
148
+ determineTypeFromRule(rule) {
179
149
  // Numeric comparisons - always number
180
150
  if (rule.condition.isEqual(AbilityCondition.greater_than) ||
181
151
  rule.condition.isEqual(AbilityCondition.less_than) ||
@@ -200,7 +170,7 @@ class AbilityParser {
200
170
  * @param resource - The resource value to analyze
201
171
  * @returns TypeScript array type as string
202
172
  */
203
- static getArrayType(resource) {
173
+ getArrayType(resource) {
204
174
  if (Array.isArray(resource)) {
205
175
  if (resource.length === 0)
206
176
  return 'any[]';
@@ -209,18 +179,18 @@ class AbilityParser {
209
179
  const elementType = elementTypes.size === 1
210
180
  ? Array.from(elementTypes)[0]
211
181
  : `(${Array.from(elementTypes).join(' | ')})`;
212
- return `${elementType}[]`;
182
+ return `readonly ${elementType}[]`;
213
183
  }
214
184
  // If resource is not an array but condition is in/not_in,
215
185
  // it expects an array of such elements
216
- return `${this.getPrimitiveType(resource)}[]`;
186
+ return `readonly ${this.getPrimitiveType(resource)}[]`;
217
187
  }
218
188
  /**
219
189
  * Gets primitive TypeScript type for a value
220
190
  * @param value - The value to analyze
221
191
  * @returns TypeScript primitive type as string
222
192
  */
223
- static getPrimitiveType(value) {
193
+ getPrimitiveType(value) {
224
194
  if (value === null)
225
195
  return 'null';
226
196
  if (value === undefined)
@@ -247,7 +217,7 @@ class AbilityParser {
247
217
  * @param flatStructure - Flat structure with dot notation paths
248
218
  * @returns Nested object structure
249
219
  */
250
- static buildNestedStructure(flatStructure) {
220
+ buildNestedStructure(flatStructure) {
251
221
  const result = {};
252
222
  Object.entries(flatStructure).forEach(([action, paths]) => {
253
223
  result[action] = {};
@@ -279,10 +249,9 @@ class AbilityParser {
279
249
  * @param structure - Nested type structure
280
250
  * @returns Formatted TypeScript type definition string
281
251
  */
282
- static formatTypeDefinitions(structure) {
252
+ formatTypeDefinitions(structure) {
283
253
  let output = '// Automatically generated by via-profit/ability\n';
284
254
  output += '// Do not edit manually\n';
285
- output += '\n/* eslint-disable */\n\n';
286
255
  output += 'export type Resources = {\n';
287
256
  // Sort actions for stable output
288
257
  const sortedActions = Object.keys(structure).sort();
@@ -300,7 +269,7 @@ class AbilityParser {
300
269
  * @param indent - Current indentation level
301
270
  * @returns Formatted string
302
271
  */
303
- static formatNestedObject(obj, indent) {
272
+ formatNestedObject(obj, indent) {
304
273
  const spaces = ' '.repeat(indent);
305
274
  let output = '';
306
275
  // Sort keys for stable output
@@ -587,8 +556,12 @@ class AbilityResolver {
587
556
  async enforce(permission, resource, environment) {
588
557
  const result = await this.resolve(permission, resource, environment);
589
558
  if (result.isDenied()) {
590
- const policyName = result.getLastMatchedPolicy()?.name?.toString() || 'unknown';
591
- throw new AbilityError(`Permission denied by policy "${policyName}"`);
559
+ const lastPolicy = result.getLastMatchedPolicy();
560
+ if (lastPolicy) {
561
+ throw new AbilityError(`Permission denied by policy "${lastPolicy.name.toString()}"`);
562
+ }
563
+ // No policy matched → implicit deny
564
+ throw new AbilityError(`Permission denied: no matching policy found (implicit deny)`);
592
565
  }
593
566
  }
594
567
  /**
@@ -660,6 +633,10 @@ class AbilityRule {
660
633
  let is = false;
661
634
  const [subjectValue, resourceValue] = this.extractValues(resource, environment);
662
635
  const isValue = (v) => typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v === null;
636
+ // always
637
+ if (AbilityCondition.always.isEqual(this.condition)) {
638
+ is = true;
639
+ }
663
640
  // equals
664
641
  if (AbilityCondition.equals.isEqual(this.condition)) {
665
642
  is = subjectValue === resourceValue;
@@ -1254,6 +1231,8 @@ class AbilityDSLToken extends AbilityCode {
1254
1231
  static LEN_LT = 'LEN_LT';
1255
1232
  static LEN_EQ = 'LEN_EQ';
1256
1233
  static NOT_EQ = 'NOT_EQ';
1234
+ static ALWAYS = 'ALWAYS';
1235
+ static NEVER = 'NEVER';
1257
1236
  static STRING = 'STRING';
1258
1237
  static NUMBER = 'NUMBER';
1259
1238
  static BOOLEAN = 'BOOLEAN';
@@ -1296,6 +1275,8 @@ class AbilityDSLLexer {
1296
1275
  'is',
1297
1276
  'or',
1298
1277
  'than',
1278
+ 'always',
1279
+ 'never',
1299
1280
  ]);
1300
1281
  constructor(input) {
1301
1282
  this.input = input;
@@ -1441,6 +1422,12 @@ class AbilityDSLLexer {
1441
1422
  }
1442
1423
  }
1443
1424
  const word = this.input.slice(start, this.pos);
1425
+ if (word === 'always') {
1426
+ return new AbilityDSLToken(AbilityDSLToken.ALWAYS, word, startLine, startColumn);
1427
+ }
1428
+ if (word === 'never') {
1429
+ return new AbilityDSLToken(AbilityDSLToken.NEVER, word, startLine, startColumn);
1430
+ }
1444
1431
  // Если есть точка — это путь (identifier или permission)
1445
1432
  if (word.includes('.')) {
1446
1433
  const last = this.tokens[this.tokens.length - 1];
@@ -1703,7 +1690,9 @@ class AbilityDSLParser {
1703
1690
  if (this.isStartOfGroup() || this.isStartOfPolicy()) {
1704
1691
  break;
1705
1692
  }
1706
- if (this.check(AbilityDSLToken.IDENTIFIER)) {
1693
+ if (this.check(AbilityDSLToken.IDENTIFIER) ||
1694
+ this.check(AbilityDSLToken.ALWAYS) ||
1695
+ this.check(AbilityDSLToken.NEVER)) {
1707
1696
  group.addRule(this.parseRule());
1708
1697
  }
1709
1698
  else {
@@ -1720,7 +1709,7 @@ class AbilityDSLParser {
1720
1709
  parseGroup() {
1721
1710
  this.consumeLeadingComments();
1722
1711
  const meta = this.takeAnnotations();
1723
- const compareToken = this.consumeOneOf([AbilityDSLToken.ALL, AbilityDSLToken.ANY], 'Expected "all" or "any"');
1712
+ const compareToken = this.consumeOneOf([AbilityDSLToken.ALL, AbilityDSLToken.ANY, AbilityDSLToken.ALWAYS, AbilityDSLToken.NEVER], 'Expected "all" or "any" or "always" or "never"');
1724
1713
  const compareMethod = compareToken.code === AbilityDSLToken.ALL ? AbilityCompare.and : AbilityCompare.or;
1725
1714
  if (this.check(AbilityDSLToken.OF)) {
1726
1715
  this.advance();
@@ -1750,11 +1739,26 @@ class AbilityDSLParser {
1750
1739
  parseRule() {
1751
1740
  this.consumeLeadingComments();
1752
1741
  const meta = this.takeAnnotations();
1753
- if (!this.check(AbilityDSLToken.IDENTIFIER)) {
1754
- this.syntaxError(`Expected identifier, got ${this.peek().code}`, this.peek());
1742
+ // if (this.check(AbilityDSLToken.ALWAYS) || this.check(AbilityDSLToken.NEVER)) {
1743
+ // // Checking that there are no extra tokens after the value
1744
+ // // (skip comments)
1745
+ // this.consumeLeadingComments();
1746
+ // const specOperator = this.consume();
1747
+ // // return new AbilityRule({
1748
+ // // subject: '',
1749
+ // // resource,
1750
+ // // condition,
1751
+ // // name: meta.name,
1752
+ // // });
1753
+ // }
1754
+ const isNeverAlways = this.check(AbilityDSLToken.ALWAYS) || this.check(AbilityDSLToken.NEVER);
1755
+ if (!isNeverAlways && !this.check(AbilityDSLToken.IDENTIFIER)) {
1756
+ this.syntaxError(`Expected identifier, but got ${this.peek().code}`, this.peek());
1755
1757
  }
1756
1758
  // Subject (e.g., "user.roles")
1757
- const subject = this.consume(AbilityDSLToken.IDENTIFIER, 'Expected field').value;
1759
+ const subject = isNeverAlways
1760
+ ? ''
1761
+ : this.consume(AbilityDSLToken.IDENTIFIER, 'Expected field').value;
1758
1762
  // Operator (e.g., "contains", "equals", "is not null")
1759
1763
  const { condition, operator } = this.parseConditionOperator();
1760
1764
  let resource;
@@ -1762,7 +1766,9 @@ class AbilityDSLParser {
1762
1766
  // Special operators that don't consume a value token.
1763
1767
  if (operator === AbilityDSLToken.EQ_NULL ||
1764
1768
  operator === AbilityDSLToken.NOT_EQ_NULL ||
1765
- operator === AbilityDSLToken.NULL) {
1769
+ operator === AbilityDSLToken.NULL ||
1770
+ operator === AbilityDSLToken.ALWAYS ||
1771
+ operator === AbilityDSLToken.NEVER) {
1766
1772
  resource = null;
1767
1773
  }
1768
1774
  else {
@@ -1794,6 +1800,16 @@ class AbilityDSLParser {
1794
1800
  */
1795
1801
  parseConditionOperator() {
1796
1802
  const savedPos = this.pos;
1803
+ // "always"
1804
+ if (this.matchWord('always')) {
1805
+ return { condition: AbilityCondition.always, operator: AbilityDSLToken.ALWAYS };
1806
+ }
1807
+ this.pos = savedPos;
1808
+ // "never"
1809
+ if (this.matchWord('never')) {
1810
+ return { condition: AbilityCondition.never, operator: AbilityDSLToken.NEVER };
1811
+ }
1812
+ this.pos = savedPos;
1797
1813
  // "length equals"
1798
1814
  if (this.matchWord('length') && this.matchWord('equals')) {
1799
1815
  return { condition: AbilityCondition.length_equals, operator: AbilityDSLToken.LEN_EQ };
@@ -1989,7 +2005,10 @@ class AbilityDSLParser {
1989
2005
  return false;
1990
2006
  }
1991
2007
  const token = this.peek();
1992
- if ((token.code === AbilityDSLToken.KEYWORD || token.code === AbilityDSLToken.IDENTIFIER) &&
2008
+ if ((token.code === AbilityDSLToken.KEYWORD ||
2009
+ token.code === AbilityDSLToken.IDENTIFIER ||
2010
+ token.code === AbilityDSLToken.ALWAYS ||
2011
+ token.code === AbilityDSLToken.NEVER) &&
1993
2012
  token.value === word) {
1994
2013
  this.advance();
1995
2014
  return true;
@@ -2134,9 +2153,6 @@ class AbilityDSLParser {
2134
2153
  }
2135
2154
  throw new AbilityDSLSyntaxError(token.line, token.column, context + '\n', finalDetails);
2136
2155
  }
2137
- getLine(lineNumber) {
2138
- return this.dsl.split(/\r?\n/)[lineNumber - 1] ?? '';
2139
- }
2140
2156
  suggest(actual, expectedTypes) {
2141
2157
  const candidates = [];
2142
2158
  for (const type of expectedTypes) {
@@ -2235,7 +2251,6 @@ exports.AbilityExplainRuleSet = AbilityExplainRuleSet;
2235
2251
  exports.AbilityInMemoryCache = AbilityInMemoryCache;
2236
2252
  exports.AbilityJSONParser = AbilityJSONParser;
2237
2253
  exports.AbilityMatch = AbilityMatch;
2238
- exports.AbilityParser = AbilityParser;
2239
2254
  exports.AbilityParserError = AbilityParserError;
2240
2255
  exports.AbilityPolicy = AbilityPolicy;
2241
2256
  exports.AbilityPolicyEffect = AbilityPolicyEffect;
@@ -2243,3 +2258,4 @@ exports.AbilityResolver = AbilityResolver;
2243
2258
  exports.AbilityResult = AbilityResult;
2244
2259
  exports.AbilityRule = AbilityRule;
2245
2260
  exports.AbilityRuleSet = AbilityRuleSet;
2261
+ exports.AbilityTypeGenerator = AbilityTypeGenerator;
@@ -1,5 +1,5 @@
1
1
  import AbilityPolicy from '../../core/AbilityPolicy';
2
- import { ResourceObject } from '../../core/AbilityParser';
2
+ import { ResourceObject } from '../../core/AbilityTypeGenerator';
3
3
  /**
4
4
  * Parser for the Ability DSL.
5
5
  *
@@ -72,7 +72,6 @@ export declare class AbilityDSLParser<Resource extends ResourceObject = Record<s
72
72
  private processCommentToken;
73
73
  private takeAnnotations;
74
74
  private syntaxError;
75
- private getLine;
76
75
  private suggest;
77
76
  private levenshteinDistance;
78
77
  private consumeOneOf;
@@ -1,5 +1,5 @@
1
1
  import AbilityCode from '../../core/AbilityCode';
2
- export type TokenType = 'EFFECT' | 'IF' | 'PERMISSION' | 'IDENTIFIER' | 'COLON' | 'COMMA' | 'DOT' | 'LBRACKET' | 'RBRACKET' | 'ALL' | 'ANY' | 'OF' | 'EOF' | 'COMMENT' | 'EQ' | 'CONTAINS' | 'IN' | 'NOT_IN' | 'NOT_CONTAINS' | 'GT' | 'GTE' | 'LT' | 'LTE' | 'NULL' | 'EQ_NULL' | 'NOT_EQ_NULL' | 'NOT_EQ' | 'LEN_GT' | 'LEN_LT' | 'LEN_EQ' | 'STRING' | 'NUMBER' | 'BOOLEAN' | 'SYMBOL' | 'KEYWORD' | 'UNKNOWN';
2
+ export type TokenType = 'EFFECT' | 'IF' | 'PERMISSION' | 'IDENTIFIER' | 'COLON' | 'COMMA' | 'DOT' | 'LBRACKET' | 'RBRACKET' | 'ALL' | 'ANY' | 'OF' | 'EOF' | 'COMMENT' | 'EQ' | 'CONTAINS' | 'IN' | 'NOT_IN' | 'NOT_CONTAINS' | 'GT' | 'GTE' | 'LT' | 'LTE' | 'NULL' | 'EQ_NULL' | 'NOT_EQ_NULL' | 'NOT_EQ' | 'LEN_GT' | 'LEN_LT' | 'LEN_EQ' | 'ALWAYS' | 'NEVER' | 'STRING' | 'NUMBER' | 'BOOLEAN' | 'SYMBOL' | 'KEYWORD' | 'UNKNOWN';
3
3
  /**
4
4
  * Represents a single token produced by the Ability DSL lexer.
5
5
  * Each token carries a type (e.g., EFFECT, IDENTIFIER, STRING) and its raw string value.
@@ -47,6 +47,8 @@ export declare class AbilityDSLToken<Code extends TokenType = TokenType> extends
47
47
  static readonly LEN_LT: TokenType;
48
48
  static readonly LEN_EQ: TokenType;
49
49
  static readonly NOT_EQ: TokenType;
50
+ static readonly ALWAYS: TokenType;
51
+ static readonly NEVER: TokenType;
50
52
  static readonly STRING: TokenType;
51
53
  static readonly NUMBER: TokenType;
52
54
  static readonly BOOLEAN: TokenType;
@@ -1,6 +1,6 @@
1
1
  import { AbilityRule, AbilityRuleConfig } from '../../core/AbilityRule';
2
2
  import { AbilityRuleSet, AbilityRuleSetConfig } from '../../core/AbilityRuleSet';
3
- import { ResourceObject } from '../../core/AbilityParser';
3
+ import { ResourceObject } from '../../core/AbilityTypeGenerator';
4
4
  import { AbilityPolicy, AbilityPolicyConfig } from '../../core/AbilityPolicy';
5
5
  export declare class AbilityJSONParser {
6
6
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@via-profit/ability",
3
3
  "support": "https://via-profit.ru",
4
- "version": "3.2.0",
4
+ "version": "3.4.0",
5
5
  "description": "Via-Profit Ability service",
6
6
  "keywords": [
7
7
  "ability",