@via-profit/ability 3.6.5 → 3.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -30,6 +30,8 @@ function brand$3(code) {
30
30
  }
31
31
  const AbilityCondition = {
32
32
  equals: brand$3('='),
33
+ defined: brand$3('defined'),
34
+ not_defined: brand$3('not_defined'),
33
35
  not_equals: brand$3('<>'),
34
36
  greater_than: brand$3('>'),
35
37
  less_than: brand$3('<'),
@@ -62,6 +64,8 @@ function fromLiteral(literal) {
62
64
  length_equals: AbilityCondition.length_equals,
63
65
  always: AbilityCondition.always,
64
66
  never: AbilityCondition.never,
67
+ defined: AbilityCondition.defined,
68
+ not_defined: AbilityCondition.not_defined,
65
69
  };
66
70
  const value = map[literal];
67
71
  if (!value) {
@@ -102,6 +106,10 @@ function toLiteral(cond) {
102
106
  return 'always';
103
107
  case AbilityCondition.never:
104
108
  return 'never';
109
+ case AbilityCondition.defined:
110
+ return 'defined';
111
+ case AbilityCondition.not_defined:
112
+ return 'not_defined';
105
113
  default:
106
114
  return 'never';
107
115
  }
@@ -124,15 +132,6 @@ const AbilityMatch = {
124
132
  disabled: brand$2('disabled'),
125
133
  };
126
134
 
127
- const colors = {
128
- reset: '\x1b[0m',
129
- green: '\x1b[32m',
130
- red: '\x1b[31m',
131
- blue: '\x1b[34m',
132
- yellow: '\x1b[33m',
133
- white: '\x1b[37m',
134
- gray: '\x1b[90m',
135
- };
136
135
  class AbilityExplain {
137
136
  type;
138
137
  children;
@@ -148,27 +147,36 @@ class AbilityExplain {
148
147
  }
149
148
  toString(indentPrefix = '', isLast = true) {
150
149
  const isMatch = this.match === AbilityMatch.match;
151
- const mark = isMatch ? `${colors.green}✓${colors.reset}` : `${colors.red}✗${colors.reset}`;
150
+ const isMismatch = this.match === AbilityMatch.mismatch;
151
+ const isPending = this.match === AbilityMatch.pending;
152
+ // const isDisabled = this.match === AbilityMatch.disabled;
153
+ const mark = isMatch
154
+ ? `<match ✓>`
155
+ : isMismatch
156
+ ? `<mismatch ✗>`
157
+ : isPending
158
+ ? `<pending …>`
159
+ : `<disabled ⊘>`;
152
160
  let label;
153
161
  switch (this.type) {
154
162
  case 'policy':
155
- label = `${colors.blue}POLICY${colors.reset}`;
163
+ label = `POLICY`;
156
164
  break;
157
165
  case 'ruleSet':
158
- label = `${colors.yellow}RULESET${colors.reset}`;
166
+ label = `RULESET`;
159
167
  break;
160
168
  default:
161
- label = `${colors.white}RULE${colors.reset}`;
169
+ label = `RULE`;
162
170
  }
163
171
  const branch = indentPrefix.length === 0
164
172
  ? ''
165
173
  : isLast
166
- ? `${colors.gray}└─${colors.reset} `
167
- : `${colors.gray}├─${colors.reset} `;
174
+ ? `└─ `
175
+ : `├─ `;
168
176
  let out = `${indentPrefix}${branch}${label} ${this.name} — ${mark}`;
169
177
  if (this.debugInfo)
170
- out += ` ${colors.gray}(${this.debugInfo})${colors.reset}`;
171
- const nextIndent = indentPrefix + (isLast ? ' ' : `${colors.gray}│ ${colors.reset}`);
178
+ out += ` (${this.debugInfo})`;
179
+ const nextIndent = indentPrefix + (isLast ? ' ' : `│ `);
172
180
  this.children.forEach((child, idx) => {
173
181
  out += '\n' + child.toString(nextIndent, idx === this.children.length - 1);
174
182
  });
@@ -181,6 +189,7 @@ class AbilityExplainRule extends AbilityExplain {
181
189
  type: 'rule',
182
190
  match: rule.state,
183
191
  name: rule.name,
192
+ debugInfo: `${rule.subject} ${rule.condition} ${JSON.stringify(rule.resource)}`,
184
193
  });
185
194
  }
186
195
  }
@@ -219,11 +228,13 @@ class AbilityResult {
219
228
  * Useful for debugging, logging, or building UI tools that visualize permission logic.
220
229
  */
221
230
  explain() {
222
- return this.strategy.policies
231
+ const resMarker = this.strategy.isDenied() ? '== DENIED==' : '== ALLOWED ==';
232
+ const policiesExplain = this.strategy.policies
223
233
  .map(policy => {
224
234
  return new AbilityExplainPolicy(policy).toString();
225
235
  })
226
236
  .join('\n');
237
+ return `${resMarker}\n${policiesExplain}\n`;
227
238
  }
228
239
  decisive() {
229
240
  return this.strategy.decisivePolicy();
@@ -243,7 +254,17 @@ class AbilityResult {
243
254
  };
244
255
  }
245
256
 
257
+ function brand$1(code) {
258
+ return code;
259
+ }
260
+ const AbilityPolicyEffect = {
261
+ deny: brand$1('deny'),
262
+ permit: brand$1('permit'),
263
+ };
264
+
246
265
  class AbilityResolver {
266
+ onDeny;
267
+ onAllow;
247
268
  StrategyClass;
248
269
  policyEntries;
249
270
  constructor(
@@ -252,6 +273,8 @@ class AbilityResolver {
252
273
  */
253
274
  policyOrListOfPolicies, strategy, options = {}) {
254
275
  const policies = this.toArray(policyOrListOfPolicies);
276
+ this.onDeny = options.onDeny;
277
+ this.onAllow = options.onAllow;
255
278
  const filtered = options.tags
256
279
  ? policies.filter(p => p.tags.some(tag => options.tags.includes(tag)))
257
280
  : policies;
@@ -289,11 +312,19 @@ class AbilityResolver {
289
312
  // 3. Use strategy
290
313
  const strategy = new this.StrategyClass(filteredPolicies);
291
314
  const effect = strategy.evaluate();
292
- return new AbilityResult(effect, strategy);
315
+ const result = new AbilityResult(effect, strategy);
316
+ if (effect === AbilityPolicyEffect.deny && this.onDeny) {
317
+ this.onDeny(result);
318
+ }
319
+ if (effect === AbilityPolicyEffect.permit && this.onAllow) {
320
+ this.onAllow(result);
321
+ }
322
+ return result;
293
323
  }
294
- enforce(permission, resource, environment) {
324
+ enforce(permission, resource, environment, options) {
295
325
  const result = this.resolve(permission, resource, environment);
296
326
  if (result.isDenied()) {
327
+ options?.onDeny && options?.onDeny(result);
297
328
  throw new AbilityError(`Permission denied`);
298
329
  }
299
330
  }
@@ -414,7 +445,7 @@ class AbilityTypeGenerator {
414
445
  environmentStructure[action] = {};
415
446
  }
416
447
  const existingEnvType = environmentStructure[action][envPath];
417
- const targetType = ruleType; // или 'unknown', если хочешь жёстко
448
+ const targetType = ruleType;
418
449
  if (existingEnvType && existingEnvType !== targetType) {
419
450
  environmentStructure[action][envPath] = `${existingEnvType} | ${targetType}`;
420
451
  }
@@ -1002,14 +1033,6 @@ class AbilityPolicy {
1002
1033
  }
1003
1034
  }
1004
1035
 
1005
- function brand$1(code) {
1006
- return code;
1007
- }
1008
- const AbilityPolicyEffect = {
1009
- deny: brand$1('deny'),
1010
- permit: brand$1('permit'),
1011
- };
1012
-
1013
1036
  /**
1014
1037
  * Represents a rule that defines a condition to be checked against a subject and resource.
1015
1038
  */
@@ -1061,6 +1084,8 @@ class AbilityRule {
1061
1084
  static valueLen = (v) => this.isString(v) || Array.isArray(v) ? v.length : null;
1062
1085
  static operatorHandlers = {
1063
1086
  [toLiteral(AbilityCondition.always)]: () => true,
1087
+ [toLiteral(AbilityCondition.defined)]: (a) => typeof a !== 'undefined',
1088
+ [toLiteral(AbilityCondition.not_defined)]: (a) => typeof a === 'undefined',
1064
1089
  [toLiteral(AbilityCondition.never)]: () => false,
1065
1090
  [toLiteral(AbilityCondition.equals)]: (a, b) => a === b,
1066
1091
  [toLiteral(AbilityCondition.not_equals)]: (a, b) => a !== b,
@@ -1600,6 +1625,7 @@ const TokenTypes = {
1600
1625
  NULL: brand('NULL'),
1601
1626
  EQ_NULL: brand('EQ_NULL'),
1602
1627
  NOT_EQ_NULL: brand('NOT_EQ_NULL'),
1628
+ DEFINED: brand('DEFINED'),
1603
1629
  NOT_EQ: brand('NOT_EQ'),
1604
1630
  LEN_GT: brand('LEN_GT'),
1605
1631
  LEN_LT: brand('LEN_LT'),
@@ -1650,9 +1676,11 @@ class AbilityDSLLexer {
1650
1676
  'true',
1651
1677
  'false',
1652
1678
  'null',
1679
+ 'defined',
1653
1680
  'contains',
1654
1681
  'includes',
1655
1682
  'length',
1683
+ 'len',
1656
1684
  'has',
1657
1685
  'in',
1658
1686
  'gt',
@@ -1866,11 +1894,11 @@ class AbilityDSLLexer {
1866
1894
  const startLine = this.line;
1867
1895
  const startColumn = this.column;
1868
1896
  const start = this.pos;
1869
- // Первый сегмент
1897
+ // First segment
1870
1898
  while (!this.isAtEnd() && /[a-zA-Z0-9_*]/.test(this.peek())) {
1871
1899
  this.advance();
1872
1900
  }
1873
- // Сегменты через точку
1901
+ // dots segments
1874
1902
  while (!this.isAtEnd() && this.peek() === '.') {
1875
1903
  this.advance(); // dot
1876
1904
  if (!/[a-zA-Z_*]/.test(this.peek())) {
@@ -1887,7 +1915,7 @@ class AbilityDSLLexer {
1887
1915
  if (word === 'never') {
1888
1916
  return new AbilityDSLToken(TokenTypes.NEVER, word, startLine, startColumn);
1889
1917
  }
1890
- // Если есть точка — это путь (identifier или permission)
1918
+ // (identifier or permission)
1891
1919
  if (word.includes('.')) {
1892
1920
  const last = this.tokens[this.tokens.length - 1];
1893
1921
  if (last?.type === TokenTypes.EFFECT) {
@@ -1897,16 +1925,13 @@ class AbilityDSLLexer {
1897
1925
  }
1898
1926
  return new AbilityDSLToken(TokenTypes.IDENTIFIER, word, startLine, startColumn);
1899
1927
  }
1900
- // Ключевые слова
1901
1928
  if (this.keywords.has(word)) {
1902
- // Эффекты
1903
1929
  if (word === 'permit' || word === 'allow') {
1904
1930
  return new AbilityDSLToken(TokenTypes.EFFECT, 'permit', startLine, startColumn);
1905
1931
  }
1906
1932
  if (word === 'deny' || word === 'forbidden') {
1907
1933
  return new AbilityDSLToken(TokenTypes.EFFECT, 'deny', startLine, startColumn);
1908
1934
  }
1909
- // Групповые ключевые слова
1910
1935
  if (word === 'all') {
1911
1936
  return new AbilityDSLToken(TokenTypes.ALL, word, startLine, startColumn);
1912
1937
  }
@@ -1919,13 +1944,15 @@ class AbilityDSLLexer {
1919
1944
  if (word === 'if') {
1920
1945
  return new AbilityDSLToken(TokenTypes.IF, word, startLine, startColumn);
1921
1946
  }
1922
- // Булевы и null
1923
1947
  if (word === 'true' || word === 'false') {
1924
1948
  return new AbilityDSLToken(TokenTypes.BOOLEAN, word, startLine, startColumn);
1925
1949
  }
1926
1950
  if (word === 'null') {
1927
1951
  return new AbilityDSLToken(TokenTypes.NULL, word, startLine, startColumn);
1928
1952
  }
1953
+ if (word === 'defined') {
1954
+ return new AbilityDSLToken(TokenTypes.DEFINED, word, startLine, startColumn);
1955
+ }
1929
1956
  if (word === 'except') {
1930
1957
  return new AbilityDSLToken(TokenTypes.EXCEPT, word, startLine, startColumn);
1931
1958
  }
@@ -2077,7 +2104,8 @@ class AbilityDSLTokenStream {
2077
2104
  if (this.eof()) {
2078
2105
  return false;
2079
2106
  }
2080
- return this.peek().type === type;
2107
+ const p = this.peek().type;
2108
+ return p === type;
2081
2109
  }
2082
2110
  match(type) {
2083
2111
  if (this.check(type)) {
@@ -2513,6 +2541,7 @@ class AbilityDSLParser {
2513
2541
  const operatorConsumesValue = operator !== TokenTypes.EQ_NULL &&
2514
2542
  operator !== TokenTypes.NOT_EQ_NULL &&
2515
2543
  operator !== TokenTypes.NULL &&
2544
+ operator !== TokenTypes.DEFINED &&
2516
2545
  operator !== TokenTypes.ALWAYS &&
2517
2546
  operator !== TokenTypes.NEVER;
2518
2547
  if (operatorConsumesValue) {
@@ -2565,42 +2594,46 @@ class AbilityDSLParser {
2565
2594
  this.stream.reset();
2566
2595
  // "length equals"
2567
2596
  this.stream.mark();
2568
- if (this.matchWord('length') && this.matchWord('equals')) {
2597
+ if ((this.matchWord('length') || this.matchWord('len')) && this.matchWord('equals')) {
2569
2598
  this.stream.commit();
2570
2599
  return { condition: AbilityCondition.length_equals, operator: TokenTypes.LEN_EQ };
2571
2600
  }
2572
2601
  this.stream.reset();
2573
2602
  // "length ="
2574
2603
  this.stream.mark();
2575
- if (this.matchWord('length') && this.matchSymbol('=')) {
2604
+ if ((this.matchWord('length') || this.matchWord('len')) && this.matchSymbol('=')) {
2576
2605
  this.stream.commit();
2577
2606
  return { condition: AbilityCondition.length_equals, operator: TokenTypes.LEN_EQ };
2578
2607
  }
2579
2608
  this.stream.reset();
2580
2609
  // "length greater than"
2581
2610
  this.stream.mark();
2582
- if (this.matchWord('length') && this.matchWord('greater') && this.matchWord('than')) {
2611
+ if ((this.matchWord('length') || this.matchWord('len')) &&
2612
+ this.matchWord('greater') &&
2613
+ this.matchWord('than')) {
2583
2614
  this.stream.commit();
2584
2615
  return { condition: AbilityCondition.length_greater_than, operator: TokenTypes.LEN_GT };
2585
2616
  }
2586
2617
  this.stream.reset();
2587
2618
  // "length >"
2588
2619
  this.stream.mark();
2589
- if (this.matchWord('length') && this.matchSymbol('>')) {
2620
+ if ((this.matchWord('length') || this.matchWord('len')) && this.matchSymbol('>')) {
2590
2621
  this.stream.commit();
2591
2622
  return { condition: AbilityCondition.length_greater_than, operator: TokenTypes.LEN_GT };
2592
2623
  }
2593
2624
  this.stream.reset();
2594
2625
  // "length less than"
2595
2626
  this.stream.mark();
2596
- if (this.matchWord('length') && this.matchWord('less') && this.matchWord('than')) {
2627
+ if ((this.matchWord('length') || this.matchWord('len')) &&
2628
+ this.matchWord('less') &&
2629
+ this.matchWord('than')) {
2597
2630
  this.stream.commit();
2598
2631
  return { condition: AbilityCondition.length_less_than, operator: TokenTypes.LEN_LT };
2599
2632
  }
2600
2633
  this.stream.reset();
2601
2634
  // "length <"
2602
2635
  this.stream.mark();
2603
- if (this.matchWord('length') && this.matchSymbol('<')) {
2636
+ if ((this.matchWord('length') || this.matchWord('len')) && this.matchSymbol('<')) {
2604
2637
  this.stream.commit();
2605
2638
  return { condition: AbilityCondition.length_less_than, operator: TokenTypes.LEN_LT };
2606
2639
  }
@@ -2728,6 +2761,20 @@ class AbilityDSLParser {
2728
2761
  }
2729
2762
  }
2730
2763
  this.stream.reset();
2764
+ // is defined
2765
+ this.stream.mark();
2766
+ if (this.matchWord('is') && this.matchWord('defined')) {
2767
+ this.stream.commit();
2768
+ return { condition: AbilityCondition.defined, operator: TokenTypes.DEFINED };
2769
+ }
2770
+ this.stream.reset();
2771
+ // is not defined
2772
+ this.stream.mark();
2773
+ if (this.matchWord('is') && this.matchWord('not') && this.matchWord('defined')) {
2774
+ this.stream.commit();
2775
+ return { condition: AbilityCondition.not_defined, operator: TokenTypes.DEFINED };
2776
+ }
2777
+ this.stream.reset();
2731
2778
  // Single token (symbol or keyword)
2732
2779
  const token = this.stream.peek();
2733
2780
  if (token.type !== TokenTypes.SYMBOL &&
@@ -2799,7 +2846,8 @@ class AbilityDSLParser {
2799
2846
  if ((token.type === TokenTypes.KEYWORD ||
2800
2847
  token.type === TokenTypes.IDENTIFIER ||
2801
2848
  token.type === TokenTypes.ALWAYS ||
2802
- token.type === TokenTypes.NEVER) &&
2849
+ token.type === TokenTypes.NEVER ||
2850
+ token.type === TokenTypes.DEFINED) &&
2803
2851
  token.value === word) {
2804
2852
  this.stream.next();
2805
2853
  return true;
@@ -2837,6 +2885,11 @@ class AbilityDSLParser {
2837
2885
  this.stream.syntaxError(`Unexpected ${token.type} in value position`, token);
2838
2886
  }
2839
2887
  this.stream.next();
2888
+ // if (token.type === TokenTypes.IDENTIFIER) {
2889
+ // this.stream.next();
2890
+ //
2891
+ // return this.parseValue();
2892
+ // }
2840
2893
  // CHECK THIS SWITCH COMPARE
2841
2894
  switch (token.type) {
2842
2895
  case TokenTypes.STRING:
@@ -2847,8 +2900,10 @@ class AbilityDSLParser {
2847
2900
  return token.value === 'true';
2848
2901
  case TokenTypes.NULL:
2849
2902
  return null;
2903
+ case TokenTypes.DEFINED:
2904
+ return typeof token.value !== 'undefined';
2850
2905
  case TokenTypes.IDENTIFIER:
2851
- return token.value;
2906
+ return null;
2852
2907
  default: {
2853
2908
  this.stream.syntaxError(`Unexpected value token "${token.value}"`, token, [
2854
2909
  TokenTypes.KEYWORD,
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.6.5",
4
+ "version": "3.7.1",
5
5
  "description": "Via-Profit Ability service",
6
6
  "keywords": [
7
7
  "ability",
@@ -34,7 +34,8 @@
34
34
  "bench": "npm run build && node ./bench/benchmark.js",
35
35
  "test": "jest",
36
36
  "lint": "tsc --noEmit && eslint --fix .",
37
- "pretty": "prettier --write ./src"
37
+ "pretty": "prettier --write ./src",
38
+ "postinstall": "node scripts/postinstall.js"
38
39
  },
39
40
  "repository": {
40
41
  "type": "git",