@via-profit/ability 3.0.1 → 3.1.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/dist/index.js CHANGED
@@ -2,7 +2,99 @@
2
2
  /******/ "use strict";
3
3
  /******/ var __webpack_modules__ = ({
4
4
 
5
- /***/ 19:
5
+ /***/ 6:
6
+ /***/ ((__unused_webpack_module, exports) => {
7
+
8
+
9
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
10
+
11
+
12
+ /***/ }),
13
+
14
+ /***/ 829:
15
+ /***/ ((__unused_webpack_module, exports) => {
16
+
17
+
18
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
19
+ exports.AbilityInMemoryCache = void 0;
20
+ class AbilityInMemoryCache {
21
+ store = new Map();
22
+ async get(key) {
23
+ const entry = this.store.get(key);
24
+ if (!entry)
25
+ return undefined;
26
+ if (Date.now() > entry.expires) {
27
+ this.store.delete(key);
28
+ return undefined;
29
+ }
30
+ return entry.value;
31
+ }
32
+ async set(key, value, ttlSeconds = 60) {
33
+ this.store.set(key, {
34
+ value,
35
+ expires: Date.now() + ttlSeconds * 1000,
36
+ });
37
+ }
38
+ serialize(input) {
39
+ return this.fastHash(this.stableStringify(input));
40
+ }
41
+ async delete(key) {
42
+ this.store.delete(key);
43
+ }
44
+ async clear() {
45
+ this.store.clear();
46
+ }
47
+ async deleteByPrefix(prefix) {
48
+ for (const key of this.store.keys()) {
49
+ if (key.startsWith(prefix)) {
50
+ this.store.delete(key);
51
+ }
52
+ }
53
+ }
54
+ fastHash(str) {
55
+ let hash = 5381;
56
+ for (let i = 0; i < str.length; i++) {
57
+ hash = (hash * 33) ^ str.charCodeAt(i);
58
+ }
59
+ return (hash >>> 0).toString(36);
60
+ }
61
+ stableStringify(obj) {
62
+ if (obj === null)
63
+ return 'null';
64
+ const type = typeof obj;
65
+ if (type === 'string')
66
+ return JSON.stringify(obj);
67
+ if (type === 'number' || type === 'boolean')
68
+ return String(obj);
69
+ if (type === 'undefined')
70
+ return 'undefined';
71
+ if (Array.isArray(obj)) {
72
+ let out = '[';
73
+ for (let i = 0; i < obj.length; i++) {
74
+ if (i > 0)
75
+ out += ',';
76
+ out += this.stableStringify(obj[i]);
77
+ }
78
+ return out + ']';
79
+ }
80
+ const keys = Object.keys(obj);
81
+ keys.sort();
82
+ let out = '{';
83
+ for (let i = 0; i < keys.length; i++) {
84
+ const k = keys[i];
85
+ if (i > 0)
86
+ out += ',';
87
+ out += k + ':' + this.stableStringify(obj[k]);
88
+ }
89
+ return out + '}';
90
+ }
91
+ }
92
+ exports.AbilityInMemoryCache = AbilityInMemoryCache;
93
+
94
+
95
+ /***/ }),
96
+
97
+ /***/ 301:
6
98
  /***/ ((__unused_webpack_module, exports) => {
7
99
 
8
100
 
@@ -29,7 +121,7 @@ exports["default"] = AbilityCode;
29
121
 
30
122
  /***/ }),
31
123
 
32
- /***/ 923:
124
+ /***/ 413:
33
125
  /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
34
126
 
35
127
 
@@ -38,7 +130,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
130
  };
39
131
  Object.defineProperty(exports, "__esModule", ({ value: true }));
40
132
  exports.AbilityCompare = void 0;
41
- const AbilityCode_1 = __importDefault(__webpack_require__(19));
133
+ const AbilityCode_1 = __importDefault(__webpack_require__(301));
42
134
  class AbilityCompare extends AbilityCode_1.default {
43
135
  static and = new AbilityCompare('and');
44
136
  static or = new AbilityCompare('or');
@@ -49,7 +141,7 @@ exports["default"] = AbilityCompare;
49
141
 
50
142
  /***/ }),
51
143
 
52
- /***/ 261:
144
+ /***/ 167:
53
145
  /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
54
146
 
55
147
 
@@ -58,35 +150,48 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
58
150
  };
59
151
  Object.defineProperty(exports, "__esModule", ({ value: true }));
60
152
  exports.AbilityCondition = void 0;
61
- const AbilityCode_1 = __importDefault(__webpack_require__(19));
62
- const AbilityError_1 = __webpack_require__(122);
153
+ const AbilityCode_1 = __importDefault(__webpack_require__(301));
154
+ const AbilityError_1 = __webpack_require__(216);
63
155
  class AbilityCondition extends AbilityCode_1.default {
64
- static equal = new AbilityCondition('=');
65
- static not_equal = new AbilityCondition('<>');
66
- static more_than = new AbilityCondition('>');
156
+ static equals = new AbilityCondition('=');
157
+ static not_equals = new AbilityCondition('<>');
158
+ static greater_than = new AbilityCondition('>');
67
159
  static less_than = new AbilityCondition('<');
68
160
  static less_or_equal = new AbilityCondition('<=');
69
- static more_or_equal = new AbilityCondition('>=');
161
+ static greater_or_equal = new AbilityCondition('>=');
70
162
  static in = new AbilityCondition('in');
71
163
  static not_in = new AbilityCondition('not in');
164
+ static contains = new AbilityCondition('contains');
165
+ static not_contains = new AbilityCondition('not contains');
166
+ static length_greater_than = new AbilityCondition('length greater than');
167
+ static length_less_than = new AbilityCondition('length less than');
168
+ static length_equals = new AbilityCondition('length equals');
72
169
  static fromLiteral(literal) {
73
170
  switch (literal) {
74
- case 'equal':
75
- return this.equal;
76
- case 'not_equal':
77
- return this.not_equal;
78
- case 'more_than':
79
- return this.more_than;
171
+ case 'equals':
172
+ return this.equals;
173
+ case 'not_equals':
174
+ return this.not_equals;
175
+ case 'greater_than':
176
+ return this.greater_than;
80
177
  case 'less_than':
81
178
  return this.less_than;
82
179
  case 'less_or_equal':
83
180
  return this.less_or_equal;
84
- case 'more_or_equal':
85
- return this.more_or_equal;
181
+ case 'greater_or_equal':
182
+ return this.greater_or_equal;
183
+ case 'contains':
184
+ return this.contains;
185
+ case 'no_contains':
186
+ return this.not_contains;
86
187
  case 'in':
87
188
  return this.in;
88
189
  case 'not_in':
89
190
  return this.not_in;
191
+ case 'length_greater_than':
192
+ return this.length_greater_than;
193
+ case 'length_equals':
194
+ return this.length_equals;
90
195
  default:
91
196
  throw new AbilityError_1.AbilityParserError(`Literal ${literal} does not found in AbilityCondition class`);
92
197
  }
@@ -108,7 +213,7 @@ exports["default"] = AbilityCondition;
108
213
 
109
214
  /***/ }),
110
215
 
111
- /***/ 122:
216
+ /***/ 216:
112
217
  /***/ ((__unused_webpack_module, exports) => {
113
218
 
114
219
 
@@ -130,7 +235,7 @@ exports.AbilityParserError = AbilityParserError;
130
235
 
131
236
  /***/ }),
132
237
 
133
- /***/ 363:
238
+ /***/ 221:
134
239
  /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
135
240
 
136
241
 
@@ -139,7 +244,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
139
244
  };
140
245
  Object.defineProperty(exports, "__esModule", ({ value: true }));
141
246
  exports.AbilityExplainPolicy = exports.AbilityExplainRuleSet = exports.AbilityExplainRule = exports.AbilityExplain = void 0;
142
- const AbilityMatch_1 = __importDefault(__webpack_require__(909));
247
+ const AbilityMatch_1 = __importDefault(__webpack_require__(247));
143
248
  class AbilityExplain {
144
249
  type;
145
250
  children;
@@ -198,7 +303,7 @@ exports.AbilityExplainPolicy = AbilityExplainPolicy;
198
303
 
199
304
  /***/ }),
200
305
 
201
- /***/ 909:
306
+ /***/ 247:
202
307
  /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
203
308
 
204
309
 
@@ -207,7 +312,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
207
312
  };
208
313
  Object.defineProperty(exports, "__esModule", ({ value: true }));
209
314
  exports.AbilityMatch = void 0;
210
- const AbilityCode_1 = __importDefault(__webpack_require__(19));
315
+ const AbilityCode_1 = __importDefault(__webpack_require__(301));
211
316
  class AbilityMatch extends AbilityCode_1.default {
212
317
  static pending = new AbilityMatch('pending');
213
318
  static match = new AbilityMatch('match');
@@ -219,7 +324,7 @@ exports["default"] = AbilityMatch;
219
324
 
220
325
  /***/ }),
221
326
 
222
- /***/ 189:
327
+ /***/ 147:
223
328
  /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
224
329
 
225
330
 
@@ -228,8 +333,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
228
333
  };
229
334
  Object.defineProperty(exports, "__esModule", ({ value: true }));
230
335
  exports.AbilityParser = void 0;
231
- const AbilityError_1 = __webpack_require__(122);
232
- const AbilityCondition_1 = __importDefault(__webpack_require__(261));
336
+ const AbilityError_1 = __webpack_require__(216);
337
+ const AbilityCondition_1 = __importDefault(__webpack_require__(167));
233
338
  class AbilityParser {
234
339
  /**
235
340
  * Sets a value in a nested object structure based on a dot/bracket notation path.
@@ -280,7 +385,7 @@ class AbilityParser {
280
385
  const typeStructure = {};
281
386
  // Iterate through all policies
282
387
  policies.forEach(policy => {
283
- const action = policy.action;
388
+ const action = policy.permission;
284
389
  // Initialize object for action if it doesn't exist
285
390
  if (!typeStructure[action]) {
286
391
  typeStructure[action] = {};
@@ -313,9 +418,9 @@ class AbilityParser {
313
418
  */
314
419
  static determineTypeFromRule(rule) {
315
420
  // Numeric comparisons - always number
316
- if (rule.condition.isEqual(AbilityCondition_1.default.more_than) ||
421
+ if (rule.condition.isEqual(AbilityCondition_1.default.greater_than) ||
317
422
  rule.condition.isEqual(AbilityCondition_1.default.less_than) ||
318
- rule.condition.isEqual(AbilityCondition_1.default.more_or_equal) ||
423
+ rule.condition.isEqual(AbilityCondition_1.default.greater_or_equal) ||
319
424
  rule.condition.isEqual(AbilityCondition_1.default.less_or_equal)) {
320
425
  return 'number';
321
426
  }
@@ -325,8 +430,8 @@ class AbilityParser {
325
430
  return this.getArrayType(rule.resource);
326
431
  }
327
432
  // Equality/Inequality operations
328
- if (rule.condition.isEqual(AbilityCondition_1.default.equal) ||
329
- rule.condition.isEqual(AbilityCondition_1.default.not_equal)) {
433
+ if (rule.condition.isEqual(AbilityCondition_1.default.equals) ||
434
+ rule.condition.isEqual(AbilityCondition_1.default.not_equals)) {
330
435
  return this.getPrimitiveType(rule.resource);
331
436
  }
332
437
  return 'any';
@@ -417,7 +522,8 @@ class AbilityParser {
417
522
  */
418
523
  static formatTypeDefinitions(structure) {
419
524
  let output = '// Automatically generated by via-profit/ability\n';
420
- output += '// Do not edit manually\n\n';
525
+ output += '// Do not edit manually\n';
526
+ output += '\n/* eslint-disable */\n\n';
421
527
  output += 'export type Resources = {\n';
422
528
  // Sort actions for stable output
423
529
  const sortedActions = Object.keys(structure).sort();
@@ -462,7 +568,7 @@ exports["default"] = AbilityParser;
462
568
 
463
569
  /***/ }),
464
570
 
465
- /***/ 844:
571
+ /***/ 278:
466
572
  /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
467
573
 
468
574
 
@@ -471,12 +577,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
471
577
  };
472
578
  Object.defineProperty(exports, "__esModule", ({ value: true }));
473
579
  exports.AbilityPolicy = void 0;
474
- const AbilityRuleSet_1 = __importDefault(__webpack_require__(402));
475
- const AbilityMatch_1 = __importDefault(__webpack_require__(909));
476
- const AbilityCompare_1 = __importDefault(__webpack_require__(923));
477
- const AbilityPolicyEffect_1 = __importDefault(__webpack_require__(277));
478
- const AbilityExplain_1 = __webpack_require__(363);
479
- const AbilityError_1 = __webpack_require__(122);
580
+ const AbilityMatch_1 = __importDefault(__webpack_require__(247));
581
+ const AbilityCompare_1 = __importDefault(__webpack_require__(413));
582
+ const AbilityExplain_1 = __webpack_require__(221);
583
+ const AbilityError_1 = __webpack_require__(216);
584
+ const AbilityJSONParser_1 = __webpack_require__(909);
585
+ const AbilityDSLParser_1 = __webpack_require__(577);
480
586
  class AbilityPolicy {
481
587
  matchState = AbilityMatch_1.default.pending;
482
588
  /**
@@ -504,14 +610,14 @@ class AbilityPolicy {
504
610
  id;
505
611
  /**
506
612
  * Running the `enforce` or `resolve` method
507
- * will select only those from all passed policies that fall under the specified action.
613
+ * will select only those from all passed policies that fall under the specified permission key.
508
614
  */
509
- action;
615
+ permission;
510
616
  constructor(params) {
511
- const { name, id, action, effect, compareMethod = AbilityCompare_1.default.and } = params;
617
+ const { name, id, permission, effect, compareMethod = AbilityCompare_1.default.and } = params;
512
618
  this.name = name;
513
619
  this.id = id;
514
- this.action = action;
620
+ this.permission = permission;
515
621
  this.effect = effect;
516
622
  this.compareMethod = compareMethod;
517
623
  }
@@ -523,25 +629,45 @@ class AbilityPolicy {
523
629
  this.ruleSet.push(ruleSet);
524
630
  return this;
525
631
  }
632
+ /**
633
+ * Add rule set to the policy
634
+ * @param ruleSets - The array of rule set to add
635
+ */
636
+ addRuleSets(ruleSets) {
637
+ for (const ruleSet of ruleSets) {
638
+ this.ruleSet.push(ruleSet);
639
+ }
640
+ return this;
641
+ }
526
642
  /**
527
643
  * Check if the policy is matched
528
- * @param resources - The resource to check
644
+ * @param resource - The resource to check
645
+ * @param environment - The user environment object
529
646
  */
530
- check(resources) {
647
+ async check(resource, environment) {
531
648
  this.matchState = AbilityMatch_1.default.mismatch;
532
649
  if (!this.ruleSet.length) {
533
650
  return this.matchState;
534
651
  }
535
- const rulesetCheckStates = this.ruleSet.reduce((collect, ruleSet) => {
536
- return collect.concat(ruleSet.check(resources));
537
- }, []);
652
+ const rulesetCheckStates = [];
653
+ for (const ruleSet of this.ruleSet) {
654
+ const state = await ruleSet.check(resource, environment);
655
+ rulesetCheckStates.push(state);
656
+ if (AbilityCompare_1.default.and.isEqual(this.compareMethod) && AbilityMatch_1.default.mismatch.isEqual(state)) {
657
+ return this.matchState; // mismatch
658
+ }
659
+ if (AbilityCompare_1.default.or.isEqual(this.compareMethod) && AbilityMatch_1.default.match.isEqual(state)) {
660
+ this.matchState = AbilityMatch_1.default.match;
661
+ return this.matchState;
662
+ }
663
+ }
538
664
  if (AbilityCompare_1.default.and.isEqual(this.compareMethod)) {
539
- if (rulesetCheckStates.every(ruleState => AbilityMatch_1.default.match.isEqual(ruleState))) {
665
+ if (rulesetCheckStates.every(s => AbilityMatch_1.default.match.isEqual(s))) {
540
666
  this.matchState = AbilityMatch_1.default.match;
541
667
  }
542
668
  }
543
669
  if (AbilityCompare_1.default.or.isEqual(this.compareMethod)) {
544
- if (rulesetCheckStates.some(ruleState => AbilityMatch_1.default.match.isEqual(ruleState))) {
670
+ if (rulesetCheckStates.some(s => AbilityMatch_1.default.match.isEqual(s))) {
545
671
  this.matchState = AbilityMatch_1.default.match;
546
672
  }
547
673
  }
@@ -558,36 +684,23 @@ class AbilityPolicy {
558
684
  * @param configs - Array of policy configurations
559
685
  * @returns Array of AbilityPolicy instances
560
686
  */
561
- static parseAll(configs) {
562
- return configs.map(config => this.parse(config));
687
+ static fromJSONAll(configs) {
688
+ return AbilityJSONParser_1.AbilityJSONParser.parse(configs);
563
689
  }
564
690
  /**
565
691
  * Parse the config JSON format to Policy class instance
566
692
  */
567
- static parse(config) {
568
- const { id, name, ruleSet, compareMethod, action, effect } = config;
569
- // Create the empty policy
570
- const policy = new AbilityPolicy({
571
- name,
572
- id,
573
- action,
574
- effect: new AbilityPolicyEffect_1.default(effect),
575
- });
576
- policy.compareMethod = new AbilityCompare_1.default(compareMethod);
577
- ruleSet.forEach(ruleSetConfig => {
578
- policy.addRuleSet(AbilityRuleSet_1.default.parse(ruleSetConfig));
579
- });
580
- return policy;
693
+ static fromJSON(config) {
694
+ return AbilityJSONParser_1.AbilityJSONParser.parsePolicy(config);
581
695
  }
582
- export() {
583
- return {
584
- id: this.id.toString(),
585
- name: this.name.toString(),
586
- compareMethod: this.compareMethod.code.toString(),
587
- ruleSet: this.ruleSet.map(rule => rule.export()),
588
- action: this.action,
589
- effect: this.effect.code,
590
- };
696
+ static fromDSL(dsl) {
697
+ return new AbilityDSLParser_1.AbilityDSLParser(dsl).parse()[0];
698
+ }
699
+ toJSON() {
700
+ return AbilityJSONParser_1.AbilityJSONParser.toJSON([this])[0];
701
+ }
702
+ toString() {
703
+ return JSON.stringify(this.toJSON());
591
704
  }
592
705
  }
593
706
  exports.AbilityPolicy = AbilityPolicy;
@@ -596,7 +709,7 @@ exports["default"] = AbilityPolicy;
596
709
 
597
710
  /***/ }),
598
711
 
599
- /***/ 277:
712
+ /***/ 179:
600
713
  /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
601
714
 
602
715
 
@@ -605,7 +718,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
605
718
  };
606
719
  Object.defineProperty(exports, "__esModule", ({ value: true }));
607
720
  exports.AbilityPolicyEffect = void 0;
608
- const AbilityCode_1 = __importDefault(__webpack_require__(19));
721
+ const AbilityCode_1 = __importDefault(__webpack_require__(301));
609
722
  class AbilityPolicyEffect extends AbilityCode_1.default {
610
723
  static deny = new AbilityPolicyEffect('deny');
611
724
  static permit = new AbilityPolicyEffect('permit');
@@ -616,7 +729,7 @@ exports["default"] = AbilityPolicyEffect;
616
729
 
617
730
  /***/ }),
618
731
 
619
- /***/ 668:
732
+ /***/ 634:
620
733
  /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
621
734
 
622
735
 
@@ -625,96 +738,159 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
625
738
  };
626
739
  Object.defineProperty(exports, "__esModule", ({ value: true }));
627
740
  exports.AbilityResolver = void 0;
628
- const AbilityPolicyEffect_1 = __importDefault(__webpack_require__(277));
629
- const AbilityMatch_1 = __importDefault(__webpack_require__(909));
630
- const AbilityError_1 = __webpack_require__(122);
631
- const AbilityExplain_1 = __webpack_require__(363);
741
+ const AbilityError_1 = __webpack_require__(216);
742
+ const AbilityResult_1 = __webpack_require__(941);
743
+ const AbilityMatch_1 = __importDefault(__webpack_require__(247));
632
744
  class AbilityResolver {
633
745
  policies;
634
- constructor(policyOrListOfPolicies) {
746
+ cache;
747
+ constructor(
748
+ /**
749
+ * `Important!` The incorrect Resources type was intentionally passed to AbilityPolicy so that TypeScript could suggest the name of the permission and the structure of its resource in the parse method.
750
+ */
751
+ policyOrListOfPolicies, options) {
752
+ const { cache } = options || {};
753
+ this.cache = cache;
635
754
  this.policies = Array.isArray(policyOrListOfPolicies)
636
755
  ? policyOrListOfPolicies
637
756
  : [policyOrListOfPolicies];
638
757
  }
639
758
  /**
640
- * Resolve policy for the resource and action
759
+ * Resolve policy for the resource and permission key
641
760
  *
642
- @param action - Action
761
+ * @param permission - Permission key
643
762
  * @param resource - Resource
763
+ * @param environment
644
764
  */
645
- resolve(action, resource) {
646
- const filteredPolicies = this.policies.filter(policy => {
647
- return AbilityResolver.isInActionContain(policy.action, String(action));
648
- });
649
- filteredPolicies.map(policy => policy.check(resource));
650
- this.policies = filteredPolicies;
651
- return this;
765
+ async resolve(permission, resource, environment) {
766
+ const filteredPolicies = this.policies.filter(policy => AbilityResolver.isInPermissionContain(policy.permission, String(permission).replace(/^permission\./, '')));
767
+ for (const policy of filteredPolicies) {
768
+ const cacheKey = this.cache ? this.makeCacheKey(policy.id, resource, environment) : '';
769
+ // cache
770
+ if (this.cache) {
771
+ const cached = await this.cache.get(cacheKey);
772
+ if (cached !== undefined) {
773
+ policy.matchState = cached;
774
+ continue;
775
+ }
776
+ }
777
+ const policyMatchState = await policy.check(resource, environment);
778
+ if (policyMatchState === AbilityMatch_1.default.pending) {
779
+ throw new AbilityError_1.AbilityError(`The policy "${policy.name}" is still in a pending state. Make sure to call "check" to evaluate the policy before resolving permissions.`);
780
+ }
781
+ if (this.cache) {
782
+ await this.cache.set(cacheKey, policyMatchState);
783
+ }
784
+ }
785
+ return new AbilityResult_1.AbilityResult(filteredPolicies);
652
786
  }
653
- resolveWithExplain(action, resource) {
654
- return this.resolve(action, resource).policies.map(policy => {
655
- return new AbilityExplain_1.AbilityExplainPolicy(policy);
787
+ async enforce(permission, resource, environment) {
788
+ const result = await this.resolve(permission, resource, environment);
789
+ if (result.isDenied()) {
790
+ const policyName = result.getLastMatchedPolicy()?.name?.toString() || 'unknown';
791
+ throw new AbilityError_1.AbilityError(`Permission denied by policy "${policyName}"`);
792
+ }
793
+ }
794
+ /**
795
+ * Check if the permission key is contained in another permission key
796
+ * @param permissionA - The first permission to check
797
+ * @param permissionB - The second permission to check
798
+ */
799
+ static isInPermissionContain(permissionA, permissionB) {
800
+ const A = permissionA.split('.');
801
+ const B = permissionB.split('.');
802
+ const [longer, shorter] = A.length >= B.length ? [A, B] : [B, A];
803
+ return shorter.every((chunk, i) => {
804
+ return chunk === '*' || longer[i] === '*' || chunk === longer[i];
656
805
  });
657
806
  }
658
- enforce(action, resource) {
659
- const resolver = this.resolve(action, resource);
660
- if (resolver.isDeny()) {
661
- throw new AbilityError_1.AbilityError(resolver.getMatchedPolicy()?.name?.toString() || 'Unknown permission error');
807
+ makeCacheKey(policyId, resource, environment) {
808
+ if (!this.cache) {
809
+ return '';
662
810
  }
811
+ return `policy:${policyId}:res:${this.cache.serialize(resource)}:env:${this.cache.serialize(environment)}`;
812
+ }
813
+ async invalidatePolicy(policyId) {
814
+ await this.cache?.deleteByPrefix(policyId);
663
815
  }
816
+ async invalidateCache() {
817
+ await this.cache?.clear();
818
+ }
819
+ }
820
+ exports.AbilityResolver = AbilityResolver;
821
+ exports["default"] = AbilityResolver;
822
+
823
+
824
+ /***/ }),
825
+
826
+ /***/ 941:
827
+ /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
828
+
829
+
830
+ var __importDefault = (this && this.__importDefault) || function (mod) {
831
+ return (mod && mod.__esModule) ? mod : { "default": mod };
832
+ };
833
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
834
+ exports.AbilityResult = void 0;
835
+ const AbilityExplain_1 = __webpack_require__(221);
836
+ const AbilityMatch_1 = __importDefault(__webpack_require__(247));
837
+ const AbilityPolicyEffect_1 = __importDefault(__webpack_require__(179));
838
+ class AbilityResult {
664
839
  /**
665
- * Get the last effect of the policy
840
+ * Already checked policies (after call the policy.check())
841
+ */
842
+ policies;
843
+ constructor(policies) {
844
+ this.policies = policies;
845
+ }
846
+ /**
847
+ * Returns a list of explanations for each policy involved in the ability evaluation.
848
+ * Each item describes how a specific policy contributed to the final permission result.
666
849
  *
667
- * @returns {AbilityPolicyEffect | null}
850
+ * Useful for debugging, logging, or building UI tools that visualize permission logic.
668
851
  */
669
- getEffect() {
670
- const effects = this.policies.reduce((collect, policy, _index) => {
671
- if (policy.matchState.isEqual(AbilityMatch_1.default.match)) {
672
- return collect.concat(policy.effect);
852
+ explain() {
853
+ return this.policies.map(policy => {
854
+ return new AbilityExplain_1.AbilityExplainPolicy(policy);
855
+ });
856
+ }
857
+ getLastMatchedPolicy() {
858
+ for (let i = this.policies.length - 1; i >= 0; i--) {
859
+ if (this.policies[i].matchState.isEqual(AbilityMatch_1.default.match)) {
860
+ return this.policies[i];
673
861
  }
674
- return collect;
675
- }, []);
676
- if (effects.length) {
677
- return effects[effects.length - 1];
678
862
  }
679
863
  return null;
680
864
  }
681
- isPermit() {
682
- const effect = this.getEffect();
683
- return effect !== null && effect.isEqual(AbilityPolicyEffect_1.default.permit);
865
+ isAllowed() {
866
+ const effect = this.getLastEffectOfMatchedPolicy();
867
+ return effect?.isEqual(AbilityPolicyEffect_1.default.permit) ?? false;
684
868
  }
685
- isDeny() {
686
- const effect = this.getEffect();
687
- return effect !== null && effect.isEqual(AbilityPolicyEffect_1.default.deny);
688
- }
689
- getMatchedPolicy() {
690
- const matchedPolicies = this.policies.filter(policy => policy.matchState.isEqual(AbilityMatch_1.default.match));
691
- const lastPolicy = matchedPolicies.length ? matchedPolicies[matchedPolicies.length - 1] : null;
692
- return lastPolicy || null;
869
+ isDenied() {
870
+ const effect = this.getLastEffectOfMatchedPolicy();
871
+ return effect?.isEqual(AbilityPolicyEffect_1.default.deny) ?? true;
693
872
  }
694
873
  /**
695
- * Check if the action is contained in another action
696
- * @param actionA - The first action to check
697
- * @param actionB - The second action to check
874
+ * Get the last effect of the policy
875
+ *
876
+ * @returns {AbilityPolicyEffect | null}
698
877
  */
699
- static isInActionContain(actionA, actionB) {
700
- const actionAArray = String(actionA).split('.');
701
- const actionBArray = String(actionB).split('.');
702
- const a = actionAArray.length >= actionBArray.length ? actionAArray : actionBArray;
703
- const b = actionBArray.length <= actionAArray.length ? actionBArray : actionAArray;
704
- const c = a.reduce((acc, chunk, index) => {
705
- const iterationRes = chunk === b[index] || b[index] === '*' || chunk === '*';
706
- return acc.concat(iterationRes);
707
- }, []);
708
- return c.every(Boolean);
878
+ getLastEffectOfMatchedPolicy() {
879
+ for (let i = this.policies.length - 1; i >= 0; i--) {
880
+ const p = this.policies[i];
881
+ if (p.matchState.isEqual(AbilityMatch_1.default.match)) {
882
+ return p.effect;
883
+ }
884
+ }
885
+ return null;
709
886
  }
710
887
  }
711
- exports.AbilityResolver = AbilityResolver;
712
- exports["default"] = AbilityResolver;
888
+ exports.AbilityResult = AbilityResult;
713
889
 
714
890
 
715
891
  /***/ }),
716
892
 
717
- /***/ 476:
893
+ /***/ 306:
718
894
  /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
719
895
 
720
896
 
@@ -723,8 +899,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
723
899
  };
724
900
  Object.defineProperty(exports, "__esModule", ({ value: true }));
725
901
  exports.AbilityRule = void 0;
726
- const AbilityMatch_1 = __importDefault(__webpack_require__(909));
727
- const AbilityCondition_1 = __importDefault(__webpack_require__(261));
902
+ const AbilityMatch_1 = __importDefault(__webpack_require__(247));
903
+ const AbilityCondition_1 = __importDefault(__webpack_require__(167));
904
+ const AbilityJSONParser_1 = __webpack_require__(909);
728
905
  /**
729
906
  * Represents a rule that defines a condition to be checked against a subject and resource.
730
907
  */
@@ -743,11 +920,11 @@ class AbilityRule {
743
920
  state = AbilityMatch_1.default.pending;
744
921
  /**
745
922
  * Creates an instance of AbilityRule.
746
- * @param {string} options.id - The unique identifier of the rule.
747
- * @param {string} options.name - The name of the rule.
748
- * @param {AbilityCondition} options.condition - The condition to evaluate.
749
- * @param {string} options.subject - The subject of the rule.
750
- * @param {string} options.resource - The resource to compare against.
923
+ * @param {string} params.id - The unique identifier of the rule.
924
+ * @param {string} params.name - The name of the rule.
925
+ * @param {AbilityCondition} params.condition - The condition to evaluate.
926
+ * @param {string} params.subject - The subject of the rule.
927
+ * @param {string} params.resource - The resource to compare against.
751
928
  * @param params
752
929
  */
753
930
  constructor(params) {
@@ -761,54 +938,139 @@ class AbilityRule {
761
938
  /**
762
939
  * Check if the rule is matched
763
940
  * @param resource - The resource to check
941
+ * @param environment
764
942
  */
765
- check(resource) {
943
+ async check(resource, environment) {
766
944
  let is = false;
767
- const [valueS, valueO] = this.extractValues(resource);
945
+ const [subjectValue, resourceValue] = this.extractValues(resource, environment);
946
+ const isValue = (v) => typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v === null;
947
+ // equals
948
+ if (AbilityCondition_1.default.equals.isEqual(this.condition)) {
949
+ is = subjectValue === resourceValue;
950
+ }
951
+ // not equals
952
+ if (AbilityCondition_1.default.not_equals.isEqual(this.condition)) {
953
+ is = subjectValue !== resourceValue;
954
+ }
955
+ // less than
768
956
  if (AbilityCondition_1.default.less_than.isEqual(this.condition)) {
769
- is = Number(valueS) < Number(valueO);
957
+ if (typeof subjectValue === 'number' && typeof resourceValue === 'number') {
958
+ is = subjectValue < resourceValue;
959
+ }
770
960
  }
961
+ // less or equal
771
962
  if (AbilityCondition_1.default.less_or_equal.isEqual(this.condition)) {
772
- is = Number(valueS) <= Number(valueO);
963
+ if (typeof subjectValue === 'number' && typeof resourceValue === 'number') {
964
+ is = subjectValue <= resourceValue;
965
+ }
773
966
  }
774
- if (AbilityCondition_1.default.more_than.isEqual(this.condition)) {
775
- is = Number(valueS) > Number(valueO);
967
+ // more than
968
+ if (AbilityCondition_1.default.greater_than.isEqual(this.condition)) {
969
+ if (typeof subjectValue === 'number' && typeof resourceValue === 'number') {
970
+ is = subjectValue > resourceValue;
971
+ }
776
972
  }
777
- if (AbilityCondition_1.default.more_or_equal.isEqual(this.condition)) {
778
- is = Number(valueS) >= Number(valueO);
973
+ // more or equal
974
+ if (AbilityCondition_1.default.greater_or_equal.isEqual(this.condition)) {
975
+ if (typeof subjectValue === 'number' && typeof resourceValue === 'number') {
976
+ is = subjectValue >= resourceValue;
977
+ }
779
978
  }
780
- if (AbilityCondition_1.default.equal.isEqual(this.condition)) {
781
- is = valueS === valueO;
979
+ // in
980
+ if (AbilityCondition_1.default.in.isEqual(this.condition)) {
981
+ // value in array
982
+ if (isValue(subjectValue) && Array.isArray(resourceValue)) {
983
+ is = resourceValue.includes(subjectValue);
984
+ }
985
+ // array intersects array
986
+ else if (Array.isArray(subjectValue) && Array.isArray(resourceValue)) {
987
+ is = subjectValue.some(v => resourceValue.includes(v));
988
+ }
782
989
  }
783
- if (AbilityCondition_1.default.not_equal.isEqual(this.condition)) {
784
- is = valueS !== valueO;
990
+ // not in
991
+ if (AbilityCondition_1.default.not_in.isEqual(this.condition)) {
992
+ if (isValue(subjectValue) && Array.isArray(resourceValue)) {
993
+ is = !resourceValue.includes(subjectValue);
994
+ }
995
+ else if (Array.isArray(subjectValue) && Array.isArray(resourceValue)) {
996
+ is = !subjectValue.some(v => resourceValue.includes(v));
997
+ }
785
998
  }
786
- if (AbilityCondition_1.default.in.isEqual(this.condition)) {
787
- // [<some>] and [<some>]
788
- if (Array.isArray(valueS) && Array.isArray(valueO)) {
789
- is = valueS.some(v => valueO.find(v1 => v1 === v));
999
+ // contains
1000
+ if (AbilityCondition_1.default.contains.isEqual(this.condition)) {
1001
+ // array contains value
1002
+ if (Array.isArray(subjectValue) && isValue(resourceValue)) {
1003
+ is = subjectValue.includes(resourceValue);
790
1004
  }
791
- // <some> and [<some>]
792
- if ((typeof valueS === 'string' || typeof valueS === 'number') && Array.isArray(valueO)) {
793
- is = valueO.includes(valueS);
1005
+ // array intersects array
1006
+ else if (Array.isArray(subjectValue) && Array.isArray(resourceValue)) {
1007
+ is = subjectValue.some(v => resourceValue.includes(v));
794
1008
  }
795
- // [<some>] and <some>
796
- if ((typeof valueO === 'string' || typeof valueO === 'number') && Array.isArray(valueS)) {
797
- is = valueS.includes(valueO);
1009
+ }
1010
+ // not contains
1011
+ if (AbilityCondition_1.default.not_contains.isEqual(this.condition)) {
1012
+ if (Array.isArray(subjectValue) && isValue(resourceValue)) {
1013
+ is = !subjectValue.includes(resourceValue);
1014
+ }
1015
+ else if (Array.isArray(subjectValue) && Array.isArray(resourceValue)) {
1016
+ is = !subjectValue.some(v => resourceValue.includes(v));
798
1017
  }
799
1018
  }
800
- if (AbilityCondition_1.default.not_in.isEqual(this.condition)) {
801
- // [<some>] and [<some>]
802
- if (Array.isArray(valueS) && Array.isArray(valueO)) {
803
- is = !valueS.some(v => valueO.find(v1 => v1 === v));
1019
+ // length equals
1020
+ if (AbilityCondition_1.default.length_equals.isEqual(this.condition)) {
1021
+ // foo.bar == n
1022
+ if (isValue(subjectValue) && typeof resourceValue === 'number') {
1023
+ is = String(subjectValue).length === resourceValue;
1024
+ }
1025
+ // ['foo', 'bar'] = n
1026
+ else if (Array.isArray(subjectValue) && typeof resourceValue === 'number') {
1027
+ is = subjectValue.length === resourceValue;
1028
+ }
1029
+ // ['foo', 'bar'] = ['baz', 'taz']
1030
+ else if (Array.isArray(subjectValue) && Array.isArray(resourceValue)) {
1031
+ is = subjectValue.length === resourceValue.length;
1032
+ }
1033
+ // 'foo' = 'bar'
1034
+ else if (typeof subjectValue === 'string' && typeof resourceValue === 'string') {
1035
+ is = subjectValue.length === resourceValue.length;
1036
+ }
1037
+ }
1038
+ // length greater than
1039
+ if (AbilityCondition_1.default.length_greater_than.isEqual(this.condition)) {
1040
+ // foo.bar > n
1041
+ if (isValue(subjectValue) && typeof resourceValue === 'number') {
1042
+ is = String(subjectValue).length > resourceValue;
804
1043
  }
805
- // <some> and [<some>]
806
- if ((typeof valueS === 'string' || typeof valueS === 'number') && Array.isArray(valueO)) {
807
- is = !valueO.includes(valueS);
1044
+ // ['foo', 'bar'] > n
1045
+ else if (Array.isArray(subjectValue) && typeof resourceValue === 'number') {
1046
+ is = subjectValue.length > resourceValue;
808
1047
  }
809
- // [<some>] and <some>
810
- if ((typeof valueO === 'string' || typeof valueO === 'number') && Array.isArray(valueS)) {
811
- is = !valueS.includes(valueO);
1048
+ // ['foo', 'bar'] > ['baz', 'taz']
1049
+ else if (Array.isArray(subjectValue) && Array.isArray(resourceValue)) {
1050
+ is = subjectValue.length > resourceValue.length;
1051
+ }
1052
+ // 'foo' > 'bar'
1053
+ else if (typeof subjectValue === 'string' && typeof resourceValue === 'string') {
1054
+ is = subjectValue.length > resourceValue.length;
1055
+ }
1056
+ }
1057
+ // length greater than
1058
+ if (AbilityCondition_1.default.length_less_than.isEqual(this.condition)) {
1059
+ // foo.bar < n
1060
+ if (isValue(subjectValue) && typeof resourceValue === 'number') {
1061
+ is = String(subjectValue).length < resourceValue;
1062
+ }
1063
+ // ['foo', 'bar'] < n
1064
+ else if (Array.isArray(subjectValue) && typeof resourceValue === 'number') {
1065
+ is = subjectValue.length < resourceValue;
1066
+ }
1067
+ // ['foo', 'bar'] < ['baz', 'taz']
1068
+ else if (Array.isArray(subjectValue) && Array.isArray(resourceValue)) {
1069
+ is = subjectValue.length < resourceValue.length;
1070
+ }
1071
+ // 'foo' < 'bar'
1072
+ else if (typeof subjectValue === 'string' && typeof resourceValue === 'string') {
1073
+ is = subjectValue.length < resourceValue.length;
812
1074
  }
813
1075
  }
814
1076
  this.state = is ? AbilityMatch_1.default.match : AbilityMatch_1.default.mismatch;
@@ -817,29 +1079,44 @@ class AbilityRule {
817
1079
  /**
818
1080
  * Extract values from the resourceData
819
1081
  * @param resourceData - The resourceData to extract values from
1082
+ * @param environment - Environment data
820
1083
  */
821
- extractValues(resourceData) {
822
- let leftSideValue;
823
- let rightSideValue;
824
- if (resourceData === null || typeof resourceData === 'undefined') {
1084
+ extractValues(resourceData, environment) {
1085
+ let subjectValue;
1086
+ let resourceValue;
1087
+ if ((resourceData === null || typeof resourceData === 'undefined') &&
1088
+ (environment === null || typeof environment === 'undefined')) {
825
1089
  return [NaN, NaN];
826
1090
  }
827
- const isPath = (str) => {
828
- return typeof str === 'string' && str.match(/\./g) !== null;
829
- };
830
- if (isPath(this.subject)) {
831
- leftSideValue = this.getDotNotationValue(resourceData, this.subject);
1091
+ // left side resolve
1092
+ if (this.subject.includes('.')) {
1093
+ // if is environment
1094
+ if (this.subject.startsWith('env.') && typeof environment !== 'undefined') {
1095
+ subjectValue = this.getDotNotationValue(environment, this.subject.replace(/^env\./, ''));
1096
+ // if is resource
1097
+ }
1098
+ else {
1099
+ subjectValue = this.getDotNotationValue(resourceData, this.subject);
1100
+ }
832
1101
  }
833
1102
  else {
834
- leftSideValue = this.subject;
1103
+ subjectValue = this.subject;
835
1104
  }
836
- if (isPath(this.resource)) {
837
- rightSideValue = this.getDotNotationValue(resourceData, this.resource);
1105
+ // right side resolve
1106
+ if (typeof this.resource === 'string' && this.resource.includes('.')) {
1107
+ // if is environment
1108
+ if (this.resource.startsWith('env.') && typeof environment !== 'undefined') {
1109
+ resourceValue = this.getDotNotationValue(environment, this.resource.replace(/^env\./, ''));
1110
+ }
1111
+ else {
1112
+ // if is resource
1113
+ resourceValue = this.getDotNotationValue(resourceData, this.resource);
1114
+ }
838
1115
  }
839
1116
  else {
840
- rightSideValue = this.resource;
1117
+ resourceValue = this.resource;
841
1118
  }
842
- return [leftSideValue, rightSideValue];
1119
+ return [subjectValue, resourceValue];
843
1120
  }
844
1121
  /**
845
1122
  * Get the value of the object by dot notation
@@ -850,7 +1127,7 @@ class AbilityRule {
850
1127
  const arr = desc.split('.');
851
1128
  while (arr.length && resource) {
852
1129
  const comp = arr.shift() || '';
853
- const match = new RegExp('(.+)\\[([0-9]*)\\]').exec(comp);
1130
+ const match = new RegExp('(.+)\\[([0-9]*)]').exec(comp);
854
1131
  if (match !== null && match.length == 3) {
855
1132
  const arrayData = {
856
1133
  arrName: match[1],
@@ -869,31 +1146,36 @@ class AbilityRule {
869
1146
  }
870
1147
  return resource;
871
1148
  }
872
- static parse(config) {
873
- const { id, name, subject, resource, condition } = config;
1149
+ toString() {
1150
+ return `AbilityRule: ${this.name} condition: ${this.condition.code} subject: "${this.subject?.toString()}" resource: "${this.resource?.toString()}"`;
1151
+ }
1152
+ static fromJSON(config) {
1153
+ return AbilityJSONParser_1.AbilityJSONParser.parseRule(config);
1154
+ }
1155
+ static equals(subject, resource) {
874
1156
  return new AbilityRule({
875
- id,
876
- name,
1157
+ condition: AbilityCondition_1.default.equals,
877
1158
  subject,
878
1159
  resource,
879
- condition: new AbilityCondition_1.default(condition),
880
1160
  });
881
1161
  }
882
- /**
883
- * Export the rule to config object
884
- */
885
- export() {
886
- return {
887
- id: this.id,
888
- name: this.name,
889
- subject: this.subject,
890
- resource: this.resource,
891
- condition: this.condition.code,
892
- };
1162
+ static notEquals(subject, resource) {
1163
+ return new AbilityRule({
1164
+ condition: AbilityCondition_1.default.not_equals,
1165
+ subject,
1166
+ resource,
1167
+ });
893
1168
  }
894
- static equal(subject, resource) {
1169
+ static contains(subject, resource) {
895
1170
  return new AbilityRule({
896
- condition: AbilityCondition_1.default.equal,
1171
+ condition: AbilityCondition_1.default.contains,
1172
+ subject,
1173
+ resource,
1174
+ });
1175
+ }
1176
+ static notContains(subject, resource) {
1177
+ return new AbilityRule({
1178
+ condition: AbilityCondition_1.default.not_contains,
897
1179
  subject,
898
1180
  resource,
899
1181
  });
@@ -914,7 +1196,7 @@ class AbilityRule {
914
1196
  }
915
1197
  static notEqual(subject, resource) {
916
1198
  return new AbilityRule({
917
- condition: AbilityCondition_1.default.not_equal,
1199
+ condition: AbilityCondition_1.default.not_equals,
918
1200
  subject,
919
1201
  resource,
920
1202
  });
@@ -935,14 +1217,14 @@ class AbilityRule {
935
1217
  }
936
1218
  static moreThan(subject, resource) {
937
1219
  return new AbilityRule({
938
- condition: AbilityCondition_1.default.more_than,
1220
+ condition: AbilityCondition_1.default.greater_than,
939
1221
  subject,
940
1222
  resource,
941
1223
  });
942
1224
  }
943
1225
  static moreOrEqual(subject, resource) {
944
1226
  return new AbilityRule({
945
- condition: AbilityCondition_1.default.more_or_equal,
1227
+ condition: AbilityCondition_1.default.greater_or_equal,
946
1228
  subject,
947
1229
  resource,
948
1230
  });
@@ -954,7 +1236,7 @@ exports["default"] = AbilityRule;
954
1236
 
955
1237
  /***/ }),
956
1238
 
957
- /***/ 402:
1239
+ /***/ 56:
958
1240
  /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
959
1241
 
960
1242
 
@@ -963,9 +1245,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
963
1245
  };
964
1246
  Object.defineProperty(exports, "__esModule", ({ value: true }));
965
1247
  exports.AbilityRuleSet = void 0;
966
- const AbilityRule_1 = __importDefault(__webpack_require__(476));
967
- const AbilityCompare_1 = __importDefault(__webpack_require__(923));
968
- const AbilityMatch_1 = __importDefault(__webpack_require__(909));
1248
+ const AbilityCompare_1 = __importDefault(__webpack_require__(413));
1249
+ const AbilityMatch_1 = __importDefault(__webpack_require__(247));
1250
+ const AbilityJSONParser_1 = __webpack_require__(909);
969
1251
  class AbilityRuleSet {
970
1252
  state = AbilityMatch_1.default.pending;
971
1253
  /**
@@ -989,7 +1271,7 @@ class AbilityRuleSet {
989
1271
  id;
990
1272
  constructor(params) {
991
1273
  const { name, id, compareMethod } = params;
992
- this.name = name || 'No name';
1274
+ this.name = name || '';
993
1275
  this.id = id || this.name;
994
1276
  this.compareMethod = compareMethod;
995
1277
  }
@@ -1001,50 +1283,43 @@ class AbilityRuleSet {
1001
1283
  rules.forEach(rule => this.addRule(rule));
1002
1284
  return this;
1003
1285
  }
1004
- check(resources) {
1286
+ async check(resources, environment) {
1005
1287
  this.state = AbilityMatch_1.default.mismatch;
1006
1288
  if (!this.rules.length) {
1007
1289
  return this.state;
1008
1290
  }
1009
- const ruleCheckStates = this.rules.reduce((collect, rule) => {
1010
- return collect.concat(rule.check(resources));
1011
- }, []);
1291
+ const ruleCheckStates = [];
1292
+ for (const rule of this.rules) {
1293
+ const state = await rule.check(resources, environment);
1294
+ ruleCheckStates.push(state);
1295
+ if (AbilityCompare_1.default.and.isEqual(this.compareMethod) && AbilityMatch_1.default.mismatch.isEqual(state)) {
1296
+ return this.state; // mismatch
1297
+ }
1298
+ if (AbilityCompare_1.default.or.isEqual(this.compareMethod) && AbilityMatch_1.default.match.isEqual(state)) {
1299
+ this.state = AbilityMatch_1.default.match;
1300
+ return this.state;
1301
+ }
1302
+ }
1012
1303
  if (AbilityCompare_1.default.and.isEqual(this.compareMethod)) {
1013
- if (ruleCheckStates.every(ruleState => AbilityMatch_1.default.match.isEqual(ruleState))) {
1304
+ if (ruleCheckStates.every(s => AbilityMatch_1.default.match.isEqual(s))) {
1014
1305
  this.state = AbilityMatch_1.default.match;
1015
1306
  }
1016
1307
  }
1017
1308
  if (AbilityCompare_1.default.or.isEqual(this.compareMethod)) {
1018
- if (ruleCheckStates.some(ruleState => AbilityMatch_1.default.match.isEqual(ruleState))) {
1309
+ if (ruleCheckStates.some(s => AbilityMatch_1.default.match.isEqual(s))) {
1019
1310
  this.state = AbilityMatch_1.default.match;
1020
1311
  }
1021
1312
  }
1022
1313
  return this.state;
1023
1314
  }
1315
+ toString() {
1316
+ return `AbilityRuleSet: ${this.name} compareMethod: ${this.compareMethod.code}, rules: ${this.rules.map(rule => rule.toString()).join('\n')}`;
1317
+ }
1024
1318
  /**
1025
1319
  * Parse the config JSON format to Group class instance
1026
1320
  */
1027
- static parse(config) {
1028
- const { id, name, rules, compareMethod } = config;
1029
- const ruleSet = new AbilityRuleSet({
1030
- compareMethod: new AbilityCompare_1.default(compareMethod),
1031
- name,
1032
- id,
1033
- });
1034
- // Adding rules if exists
1035
- if (rules && rules.length > 0) {
1036
- const abilityRules = rules.map(ruleConfig => AbilityRule_1.default.parse(ruleConfig));
1037
- ruleSet.addRules(abilityRules);
1038
- }
1039
- return ruleSet;
1040
- }
1041
- export() {
1042
- return {
1043
- id: this.id.toString(),
1044
- name: this.name.toString(),
1045
- compareMethod: this.compareMethod.code.toString(),
1046
- rules: this.rules.map(rule => rule.export()),
1047
- };
1321
+ static fromJSON(config) {
1322
+ return AbilityJSONParser_1.AbilityJSONParser.parseRuleSet(config);
1048
1323
  }
1049
1324
  static and(rules) {
1050
1325
  return new AbilityRuleSet({
@@ -1082,18 +1357,1207 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
1082
1357
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
1083
1358
  };
1084
1359
  Object.defineProperty(exports, "__esModule", ({ value: true }));
1085
- __exportStar(__webpack_require__(19), exports);
1086
- __exportStar(__webpack_require__(923), exports);
1087
- __exportStar(__webpack_require__(261), exports);
1088
- __exportStar(__webpack_require__(122), exports);
1360
+ __exportStar(__webpack_require__(301), exports);
1361
+ __exportStar(__webpack_require__(413), exports);
1362
+ __exportStar(__webpack_require__(167), exports);
1363
+ __exportStar(__webpack_require__(216), exports);
1364
+ __exportStar(__webpack_require__(247), exports);
1365
+ __exportStar(__webpack_require__(147), exports);
1366
+ __exportStar(__webpack_require__(278), exports);
1367
+ __exportStar(__webpack_require__(179), exports);
1368
+ __exportStar(__webpack_require__(634), exports);
1369
+ __exportStar(__webpack_require__(306), exports);
1370
+ __exportStar(__webpack_require__(56), exports);
1371
+ __exportStar(__webpack_require__(221), exports);
1372
+ __exportStar(__webpack_require__(941), exports);
1373
+ __exportStar(__webpack_require__(6), exports);
1374
+ __exportStar(__webpack_require__(829), exports);
1089
1375
  __exportStar(__webpack_require__(909), exports);
1090
- __exportStar(__webpack_require__(189), exports);
1091
- __exportStar(__webpack_require__(844), exports);
1092
- __exportStar(__webpack_require__(277), exports);
1093
- __exportStar(__webpack_require__(668), exports);
1094
- __exportStar(__webpack_require__(476), exports);
1095
- __exportStar(__webpack_require__(402), exports);
1096
- __exportStar(__webpack_require__(363), exports);
1376
+ __exportStar(__webpack_require__(577), exports);
1377
+ __exportStar(__webpack_require__(10), exports);
1378
+ __exportStar(__webpack_require__(325), exports);
1379
+
1380
+
1381
+ /***/ }),
1382
+
1383
+ /***/ 10:
1384
+ /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
1385
+
1386
+
1387
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
1388
+ exports.AbilityDSLLexer = void 0;
1389
+ const AbilityDSLToken_1 = __webpack_require__(325);
1390
+ class AbilityDSLLexer {
1391
+ input;
1392
+ pos = 0;
1393
+ tokens = [];
1394
+ line = 1;
1395
+ column = 1;
1396
+ // Список ключевых слов
1397
+ keywords = new Set([
1398
+ 'if',
1399
+ 'all',
1400
+ 'any',
1401
+ 'of',
1402
+ 'permit',
1403
+ 'allow',
1404
+ 'deny',
1405
+ 'forbidden',
1406
+ 'true',
1407
+ 'false',
1408
+ 'null',
1409
+ 'contains',
1410
+ 'includes',
1411
+ 'length',
1412
+ 'has',
1413
+ 'in',
1414
+ 'gt',
1415
+ 'lt',
1416
+ 'gte',
1417
+ 'lte',
1418
+ 'equals',
1419
+ 'greater',
1420
+ 'less',
1421
+ 'not',
1422
+ 'is',
1423
+ 'or',
1424
+ 'than',
1425
+ ]);
1426
+ constructor(input) {
1427
+ this.input = input;
1428
+ }
1429
+ tokenize() {
1430
+ while (!this.isAtEnd()) {
1431
+ this.skipWhitespace();
1432
+ if (this.isAtEnd())
1433
+ break;
1434
+ const char = this.peek();
1435
+ if (char === '#') {
1436
+ this.tokens.push(this.readComment());
1437
+ continue;
1438
+ }
1439
+ if (char === '"' || char === "'") {
1440
+ this.tokens.push(this.readString());
1441
+ continue;
1442
+ }
1443
+ if (this.isDigit(char)) {
1444
+ this.tokens.push(this.readNumber());
1445
+ continue;
1446
+ }
1447
+ if (this.isSymbol(char)) {
1448
+ this.tokens.push(this.readSymbol());
1449
+ continue;
1450
+ }
1451
+ if (this.isAlpha(char)) {
1452
+ this.tokens.push(this.readWord());
1453
+ continue;
1454
+ }
1455
+ throw new Error(`Unexpected character '${char}' at ${this.line}:${this.column}`);
1456
+ }
1457
+ this.tokens.push(new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.EOF, '', this.line, this.column));
1458
+ return this.tokens;
1459
+ }
1460
+ readComment() {
1461
+ const startLine = this.line;
1462
+ const startColumn = this.column;
1463
+ this.advance(); // skip '#'
1464
+ let value = '';
1465
+ while (!this.isAtEnd() && !this.isNewline()) {
1466
+ value += this.advance();
1467
+ }
1468
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.COMMENT, value.trim(), startLine, startColumn);
1469
+ }
1470
+ readString() {
1471
+ const startLine = this.line;
1472
+ const startColumn = this.column;
1473
+ const quote = this.advance();
1474
+ let value = '';
1475
+ let escaped = false;
1476
+ while (!this.isAtEnd()) {
1477
+ const char = this.advance();
1478
+ if (escaped) {
1479
+ value += char;
1480
+ escaped = false;
1481
+ continue;
1482
+ }
1483
+ if (char === '\\') {
1484
+ escaped = true;
1485
+ continue;
1486
+ }
1487
+ if (char === quote) {
1488
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.STRING, value, startLine, startColumn);
1489
+ }
1490
+ value += char;
1491
+ }
1492
+ throw new Error(`Unterminated string at ${startLine}:${startColumn}`);
1493
+ }
1494
+ readNumber() {
1495
+ const startLine = this.line;
1496
+ const startColumn = this.column;
1497
+ const start = this.pos;
1498
+ while (!this.isAtEnd() && this.isDigit(this.peek())) {
1499
+ this.advance();
1500
+ }
1501
+ const value = this.input.slice(start, this.pos);
1502
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.NUMBER, value, startLine, startColumn);
1503
+ }
1504
+ readSymbol() {
1505
+ const startLine = this.line;
1506
+ const startColumn = this.column;
1507
+ const char = this.advance();
1508
+ switch (char) {
1509
+ case '.':
1510
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.DOT, char, startLine, startColumn);
1511
+ case ':':
1512
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.COLON, char, startLine, startColumn);
1513
+ case ',':
1514
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.COMMA, char, startLine, startColumn);
1515
+ case '[':
1516
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.LBRACKET, char, startLine, startColumn);
1517
+ case ']':
1518
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.RBRACKET, char, startLine, startColumn);
1519
+ case '>':
1520
+ if (this.peek() === '=') {
1521
+ this.advance();
1522
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.SYMBOL, '>=', startLine, startColumn);
1523
+ }
1524
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.SYMBOL, '>', startLine, startColumn);
1525
+ case '<':
1526
+ if (this.peek() === '=') {
1527
+ this.advance();
1528
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.SYMBOL, '<=', startLine, startColumn);
1529
+ }
1530
+ if (this.peek() === '>') {
1531
+ this.advance();
1532
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.SYMBOL, '<>', startLine, startColumn);
1533
+ }
1534
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.SYMBOL, '<', startLine, startColumn);
1535
+ case '=':
1536
+ if (this.peek() === '=') {
1537
+ this.advance();
1538
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.SYMBOL, '==', startLine, startColumn);
1539
+ }
1540
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.SYMBOL, '=', startLine, startColumn);
1541
+ case '!':
1542
+ if (this.peek() === '=') {
1543
+ this.advance();
1544
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.SYMBOL, '!=', startLine, startColumn);
1545
+ }
1546
+ throw new Error(`Unexpected symbol '!' at ${this.line}:${this.column}`);
1547
+ default:
1548
+ throw new Error(`Unknown symbol '${char}' at ${this.line}:${this.column}`);
1549
+ }
1550
+ }
1551
+ readWord() {
1552
+ const startLine = this.line;
1553
+ const startColumn = this.column;
1554
+ const start = this.pos;
1555
+ // Первый сегмент
1556
+ while (!this.isAtEnd() && /[a-zA-Z0-9_*]/.test(this.peek())) {
1557
+ this.advance();
1558
+ }
1559
+ // Сегменты через точку
1560
+ while (!this.isAtEnd() && this.peek() === '.') {
1561
+ this.advance(); // dot
1562
+ if (!/[a-zA-Z_*]/.test(this.peek())) {
1563
+ break;
1564
+ }
1565
+ while (!this.isAtEnd() && /[a-zA-Z0-9_*]/.test(this.peek())) {
1566
+ this.advance();
1567
+ }
1568
+ }
1569
+ const word = this.input.slice(start, this.pos);
1570
+ // Если есть точка — это путь (identifier или permission)
1571
+ if (word.includes('.')) {
1572
+ const last = this.tokens[this.tokens.length - 1];
1573
+ if (last?.code === AbilityDSLToken_1.AbilityDSLToken.EFFECT) {
1574
+ if (word.startsWith('permission.')) {
1575
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.PERMISSION, word, startLine, startColumn);
1576
+ }
1577
+ }
1578
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.IDENTIFIER, word, startLine, startColumn);
1579
+ }
1580
+ // Ключевые слова
1581
+ if (this.keywords.has(word)) {
1582
+ // Эффекты
1583
+ if (word === 'permit' || word === 'allow') {
1584
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.EFFECT, 'permit', startLine, startColumn);
1585
+ }
1586
+ if (word === 'deny' || word === 'forbidden') {
1587
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.EFFECT, 'deny', startLine, startColumn);
1588
+ }
1589
+ // Групповые ключевые слова
1590
+ if (word === 'all') {
1591
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.ALL, word, startLine, startColumn);
1592
+ }
1593
+ if (word === 'any') {
1594
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.ANY, word, startLine, startColumn);
1595
+ }
1596
+ if (word === 'of') {
1597
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.OF, word, startLine, startColumn);
1598
+ }
1599
+ if (word === 'if') {
1600
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.IF, word, startLine, startColumn);
1601
+ }
1602
+ // Булевы и null
1603
+ if (word === 'true' || word === 'false') {
1604
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.BOOLEAN, word, startLine, startColumn);
1605
+ }
1606
+ if (word === 'null') {
1607
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.NULL, word, startLine, startColumn);
1608
+ }
1609
+ // Остальные ключевые слова (contains, in, equals, greater, less, not, is, or, than, equal)
1610
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.KEYWORD, word, startLine, startColumn);
1611
+ }
1612
+ // Если после EFFECT и нет точки — действие (например, "create")
1613
+ const lastToken = this.tokens[this.tokens.length - 1];
1614
+ if (lastToken?.code === AbilityDSLToken_1.AbilityDSLToken.EFFECT) {
1615
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.PERMISSION, word, startLine, startColumn);
1616
+ }
1617
+ // Обычный идентификатор
1618
+ return new AbilityDSLToken_1.AbilityDSLToken(AbilityDSLToken_1.AbilityDSLToken.IDENTIFIER, word, startLine, startColumn);
1619
+ }
1620
+ skipWhitespace() {
1621
+ while (!this.isAtEnd() && /\s/.test(this.peek())) {
1622
+ this.advance();
1623
+ }
1624
+ }
1625
+ isDigit(char) {
1626
+ return char >= '0' && char <= '9';
1627
+ }
1628
+ isAlpha(char) {
1629
+ return /[a-zA-Z_]/.test(char);
1630
+ }
1631
+ isSymbol(char) {
1632
+ return ['.', ':', ',', '[', ']', '>', '<', '=', '!'].includes(char);
1633
+ }
1634
+ isNewline() {
1635
+ return this.peek() === '\n';
1636
+ }
1637
+ peek() {
1638
+ return this.input[this.pos];
1639
+ }
1640
+ advance() {
1641
+ const ch = this.input[this.pos++];
1642
+ if (ch === '\n') {
1643
+ this.line++;
1644
+ this.column = 1;
1645
+ }
1646
+ else {
1647
+ this.column++;
1648
+ }
1649
+ return ch;
1650
+ }
1651
+ isAtEnd() {
1652
+ return this.pos >= this.input.length;
1653
+ }
1654
+ }
1655
+ exports.AbilityDSLLexer = AbilityDSLLexer;
1656
+
1657
+
1658
+ /***/ }),
1659
+
1660
+ /***/ 577:
1661
+ /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
1662
+
1663
+
1664
+ var __importDefault = (this && this.__importDefault) || function (mod) {
1665
+ return (mod && mod.__esModule) ? mod : { "default": mod };
1666
+ };
1667
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
1668
+ exports.AbilityDSLParser = void 0;
1669
+ const AbilityCompare_1 = __importDefault(__webpack_require__(413));
1670
+ const AbilityCondition_1 = __importDefault(__webpack_require__(167));
1671
+ const AbilityPolicy_1 = __importDefault(__webpack_require__(278));
1672
+ const AbilityPolicyEffect_1 = __importDefault(__webpack_require__(179));
1673
+ const AbilityRule_1 = __importDefault(__webpack_require__(306));
1674
+ const AbilityRuleSet_1 = __importDefault(__webpack_require__(56));
1675
+ const AbilityDSLLexer_1 = __webpack_require__(10);
1676
+ const AbilityDSLToken_1 = __webpack_require__(325);
1677
+ const AbilityDSLSyntaxError_1 = __webpack_require__(883);
1678
+ /**
1679
+ * Parser for the Ability DSL.
1680
+ *
1681
+ * Converts a DSL string into one or more AbilityPolicy instances.
1682
+ * The grammar follows the structure:
1683
+ *
1684
+ * <effect> <permission> if <group> [ <group> ... ]
1685
+ *
1686
+ * where <group> is either "all of:" or "any of:", followed by a colon,
1687
+ * and then a list of rules (one per line).
1688
+ *
1689
+ * Each rule is: <identifier> <operator> <value>
1690
+ *
1691
+ * Operators can be simple (equals, contains, in) or
1692
+ * composed (is null, is not null, greater than, less than or equal, etc.).
1693
+ */
1694
+ class AbilityDSLParser {
1695
+ dsl;
1696
+ tokens = [];
1697
+ pos = 0;
1698
+ annotationBuffer = {
1699
+ name: null,
1700
+ description: null,
1701
+ };
1702
+ constructor(dsl) {
1703
+ this.dsl = dsl;
1704
+ }
1705
+ /**
1706
+ * Main entry point: tokenize the input and parse all policies.
1707
+ * @returns Array of AbilityPolicy instances.
1708
+ */
1709
+ parse() {
1710
+ // Tokenize the entire DSL string.
1711
+ this.tokens = new AbilityDSLLexer_1.AbilityDSLLexer(this.dsl).tokenize();
1712
+ this.pos = 0;
1713
+ const policies = [];
1714
+ // Keep parsing until we've consumed all tokens.
1715
+ while (!this.isAtEnd()) {
1716
+ this.consumeLeadingComments();
1717
+ // Every policy must start with an EFFECT token.
1718
+ if (!this.isStartOfPolicy()) {
1719
+ const token = this.peek();
1720
+ this.syntaxError(`Expected policy, got ${token.code}.`, token, ['EFFECT']);
1721
+ }
1722
+ policies.push(this.parsePolicy());
1723
+ }
1724
+ return policies;
1725
+ }
1726
+ // -------------------------------------------------------------------------
1727
+ // #region Policy parsing
1728
+ // -------------------------------------------------------------------------
1729
+ /**
1730
+ * Parses a single policy from the current token position.
1731
+ *
1732
+ * Grammar:
1733
+ * policy = EFFECT PERMISSION IF (ALL | ANY) COLON ruleSets
1734
+ */
1735
+ parsePolicy() {
1736
+ this.consumeLeadingComments();
1737
+ const meta = this.takeAnnotations();
1738
+ // Effect: "permit" or "deny"
1739
+ const effectToken = this.consume(AbilityDSLToken_1.AbilityDSLToken.EFFECT, 'Expected effect');
1740
+ const effect = effectToken.value;
1741
+ // Permission: e.g. "order.update"
1742
+ const permissionToken = this.consume(AbilityDSLToken_1.AbilityDSLToken.PERMISSION, 'Expected permission');
1743
+ const permission = permissionToken.value;
1744
+ if (!permission.startsWith('permission.')) {
1745
+ return this.syntaxError(`Unexpected token. The permission key, must be starts with prefix \`permission.\`, but got \`${permission}\`.\nDid you mean \`permission.${permission}\`?`, permissionToken);
1746
+ }
1747
+ // "if" keyword
1748
+ this.consume(AbilityDSLToken_1.AbilityDSLToken.IF, 'Expected "if"');
1749
+ // Group selector: "all" or "any" – determines how the top‑level rule sets are combined.
1750
+ const compareToken = this.consumeOneOf([AbilityDSLToken_1.AbilityDSLToken.ALL, AbilityDSLToken_1.AbilityDSLToken.ANY], 'Expected "all" or "any"');
1751
+ const compareMethod = compareToken.code === AbilityDSLToken_1.AbilityDSLToken.ALL ? AbilityCompare_1.default.and : AbilityCompare_1.default.or;
1752
+ // Colon after the group keyword
1753
+ this.consume(AbilityDSLToken_1.AbilityDSLToken.COLON, 'Expected ":"');
1754
+ // Parse the list of rule sets (each "all of:" or "any of:" block)
1755
+ const ruleSets = this.parseRuleSets(compareMethod);
1756
+ // Construct the policy instance.
1757
+ return new AbilityPolicy_1.default({
1758
+ id: `${effect}:${permission}:${Math.random()}`,
1759
+ name: meta.name ?? `${effect} ${permission}`,
1760
+ permission: permission.replace(/^permission\./, ''),
1761
+ effect: effect === 'permit' ? AbilityPolicyEffect_1.default.permit : AbilityPolicyEffect_1.default.deny,
1762
+ compareMethod,
1763
+ }).addRuleSets(ruleSets);
1764
+ }
1765
+ // -------------------------------------------------------------------------
1766
+ // #region Rule set parsing (groups of rules)
1767
+ // -------------------------------------------------------------------------
1768
+ /**
1769
+ * Parses a sequence of rule sets (groups) until a new policy starts or EOF.
1770
+ */
1771
+ parseRuleSets(policyCompareMethod) {
1772
+ const sets = [];
1773
+ while (!this.isAtEnd() && !this.isStartOfPolicy()) {
1774
+ this.consumeLeadingComments();
1775
+ // Если начинается новая группа — парсим её
1776
+ if (this.isStartOfGroup()) {
1777
+ sets.push(this.parseGroup());
1778
+ continue;
1779
+ }
1780
+ // Иначе — implicit group (all-of по умолчанию)
1781
+ const meta = this.takeAnnotations();
1782
+ const group = new AbilityRuleSet_1.default({
1783
+ compareMethod: policyCompareMethod,
1784
+ name: meta.name,
1785
+ });
1786
+ // Читаем правила implicit-группы
1787
+ while (!this.isAtEnd()) {
1788
+ this.consumeLeadingComments();
1789
+ if (this.isStartOfGroup() || this.isStartOfPolicy()) {
1790
+ break;
1791
+ }
1792
+ if (this.check(AbilityDSLToken_1.AbilityDSLToken.IDENTIFIER)) {
1793
+ group.addRule(this.parseRule());
1794
+ }
1795
+ else {
1796
+ this.syntaxError(`Unexpected token in implicit group: ${this.peek().code}`, this.peek());
1797
+ }
1798
+ }
1799
+ sets.push(group);
1800
+ }
1801
+ return sets;
1802
+ }
1803
+ /**
1804
+ * Parses a single group, e.g. "all of:" or "any of:", and returns a RuleSet.
1805
+ */
1806
+ parseGroup() {
1807
+ this.consumeLeadingComments();
1808
+ const meta = this.takeAnnotations();
1809
+ const compareToken = this.consumeOneOf([AbilityDSLToken_1.AbilityDSLToken.ALL, AbilityDSLToken_1.AbilityDSLToken.ANY], 'Expected "all" or "any"');
1810
+ const compareMethod = compareToken.code === AbilityDSLToken_1.AbilityDSLToken.ALL ? AbilityCompare_1.default.and : AbilityCompare_1.default.or;
1811
+ if (this.check(AbilityDSLToken_1.AbilityDSLToken.OF)) {
1812
+ this.advance();
1813
+ }
1814
+ this.consume(AbilityDSLToken_1.AbilityDSLToken.COLON, 'Expected ":"');
1815
+ const group = new AbilityRuleSet_1.default({ compareMethod, name: meta.name });
1816
+ while (!this.isAtEnd()) {
1817
+ this.consumeLeadingComments();
1818
+ if (this.isStartOfGroup() || this.isStartOfPolicy()) {
1819
+ break;
1820
+ }
1821
+ if (this.check(AbilityDSLToken_1.AbilityDSLToken.IDENTIFIER)) {
1822
+ group.addRule(this.parseRule());
1823
+ }
1824
+ else {
1825
+ this.syntaxError(`Unexpected token in group: ${this.peek().code}`, this.peek());
1826
+ }
1827
+ }
1828
+ return group;
1829
+ }
1830
+ // -------------------------------------------------------------------------
1831
+ // #region Rule parsing
1832
+ // -------------------------------------------------------------------------
1833
+ /**
1834
+ * Parses a single rule: subject operator value
1835
+ */
1836
+ parseRule() {
1837
+ this.consumeLeadingComments();
1838
+ const meta = this.takeAnnotations();
1839
+ if (!this.check(AbilityDSLToken_1.AbilityDSLToken.IDENTIFIER)) {
1840
+ this.syntaxError(`Expected identifier, got ${this.peek().code}`, this.peek());
1841
+ }
1842
+ // Subject (e.g., "user.roles")
1843
+ const subject = this.consume(AbilityDSLToken_1.AbilityDSLToken.IDENTIFIER, 'Expected field').value;
1844
+ // Operator (e.g., "contains", "equals", "is not null")
1845
+ const { condition, operator } = this.parseConditionOperator();
1846
+ let resource;
1847
+ let beforePos = this.pos;
1848
+ // Special operators that don't consume a value token.
1849
+ if (operator === AbilityDSLToken_1.AbilityDSLToken.EQ_NULL ||
1850
+ operator === AbilityDSLToken_1.AbilityDSLToken.NOT_EQ_NULL ||
1851
+ operator === AbilityDSLToken_1.AbilityDSLToken.NULL) {
1852
+ resource = null;
1853
+ }
1854
+ else {
1855
+ beforePos = this.pos;
1856
+ resource = this.parseValue();
1857
+ }
1858
+ // Checking that there are no extra tokens after the value
1859
+ // (skip comments)
1860
+ this.consumeLeadingComments();
1861
+ const resourceToken = this.tokens[beforePos];
1862
+ if (typeof resource === 'string' &&
1863
+ resourceToken.code === AbilityDSLToken_1.AbilityDSLToken.IDENTIFIER &&
1864
+ !resourceToken.value.includes('.')) {
1865
+ this.syntaxError(`Expected comparison operator or value, got \`${resource}\``, this.tokens[beforePos], [AbilityDSLToken_1.AbilityDSLToken.KEYWORD]);
1866
+ }
1867
+ return new AbilityRule_1.default({
1868
+ subject,
1869
+ resource,
1870
+ condition,
1871
+ name: meta.name,
1872
+ });
1873
+ }
1874
+ // -------------------------------------------------------------------------
1875
+ // #region Operator parsing
1876
+ // -------------------------------------------------------------------------
1877
+ /**
1878
+ * Parses the comparison operator part of a rule.
1879
+ * Returns both the resulting AbilityCondition and the token type that was consumed.
1880
+ */
1881
+ parseConditionOperator() {
1882
+ const savedPos = this.pos;
1883
+ // "length equals"
1884
+ if (this.matchWord('length') && this.matchWord('equals')) {
1885
+ return { condition: AbilityCondition_1.default.length_equals, operator: AbilityDSLToken_1.AbilityDSLToken.LEN_EQ };
1886
+ }
1887
+ this.pos = savedPos;
1888
+ // "length ="
1889
+ if (this.matchWord('length') && this.matchSymbol('=')) {
1890
+ return { condition: AbilityCondition_1.default.length_equals, operator: AbilityDSLToken_1.AbilityDSLToken.LEN_EQ };
1891
+ }
1892
+ this.pos = savedPos;
1893
+ // "length greater than"
1894
+ if (this.matchWord('length') && this.matchWord('greater') && this.matchWord('than')) {
1895
+ return { condition: AbilityCondition_1.default.length_greater_than, operator: AbilityDSLToken_1.AbilityDSLToken.LEN_GT };
1896
+ }
1897
+ this.pos = savedPos;
1898
+ // "length >"
1899
+ if (this.matchWord('length') && this.matchSymbol('>')) {
1900
+ return { condition: AbilityCondition_1.default.length_greater_than, operator: AbilityDSLToken_1.AbilityDSLToken.LEN_GT };
1901
+ }
1902
+ this.pos = savedPos;
1903
+ // "length less than"
1904
+ if (this.matchWord('length') && this.matchWord('less') && this.matchWord('than')) {
1905
+ return { condition: AbilityCondition_1.default.length_less_than, operator: AbilityDSLToken_1.AbilityDSLToken.LEN_LT };
1906
+ }
1907
+ this.pos = savedPos;
1908
+ // "length <"
1909
+ if (this.matchWord('length') && this.matchSymbol('<')) {
1910
+ return { condition: AbilityCondition_1.default.length_less_than, operator: AbilityDSLToken_1.AbilityDSLToken.LEN_LT };
1911
+ }
1912
+ this.pos = savedPos;
1913
+ // "greater than or equal"
1914
+ if (this.matchWord('greater') &&
1915
+ this.matchWord('than') &&
1916
+ this.matchWord('or') &&
1917
+ this.matchWord('equal')) {
1918
+ return { condition: AbilityCondition_1.default.greater_or_equal, operator: AbilityDSLToken_1.AbilityDSLToken.GTE };
1919
+ }
1920
+ this.pos = savedPos;
1921
+ // greater than
1922
+ if (this.matchWord('greater') && this.matchWord('than')) {
1923
+ return { condition: AbilityCondition_1.default.greater_than, operator: AbilityDSLToken_1.AbilityDSLToken.GT };
1924
+ }
1925
+ this.pos = savedPos;
1926
+ // less than or equal
1927
+ if (this.matchWord('less') &&
1928
+ this.matchWord('than') &&
1929
+ this.matchWord('or') &&
1930
+ this.matchWord('equal')) {
1931
+ return { condition: AbilityCondition_1.default.less_or_equal, operator: AbilityDSLToken_1.AbilityDSLToken.LTE };
1932
+ }
1933
+ this.pos = savedPos;
1934
+ // less than
1935
+ if (this.matchWord('less') && this.matchWord('than')) {
1936
+ return { condition: AbilityCondition_1.default.less_than, operator: AbilityDSLToken_1.AbilityDSLToken.LT };
1937
+ }
1938
+ this.pos = savedPos;
1939
+ // "not contains"
1940
+ if (this.matchWord('not') && this.matchWord('contains')) {
1941
+ return {
1942
+ condition: AbilityCondition_1.default.not_contains,
1943
+ operator: AbilityDSLToken_1.AbilityDSLToken.NOT_CONTAINS,
1944
+ };
1945
+ }
1946
+ this.pos = savedPos;
1947
+ // "not includes"
1948
+ if (this.matchWord('not') && this.matchWord('includes')) {
1949
+ return {
1950
+ condition: AbilityCondition_1.default.not_contains,
1951
+ operator: AbilityDSLToken_1.AbilityDSLToken.NOT_CONTAINS,
1952
+ };
1953
+ }
1954
+ this.pos = savedPos;
1955
+ // "not includes"
1956
+ if (this.matchWord('not') && this.matchWord('has')) {
1957
+ return {
1958
+ condition: AbilityCondition_1.default.not_contains,
1959
+ operator: AbilityDSLToken_1.AbilityDSLToken.NOT_CONTAINS,
1960
+ };
1961
+ }
1962
+ this.pos = savedPos;
1963
+ // "is equals"
1964
+ if (this.matchWord('is') && this.matchWord('equals')) {
1965
+ return { condition: AbilityCondition_1.default.equals, operator: AbilityDSLToken_1.AbilityDSLToken.EQ };
1966
+ }
1967
+ this.pos = savedPos;
1968
+ // not equal
1969
+ if (this.matchWord('not') && this.matchWord('equals')) {
1970
+ return { condition: AbilityCondition_1.default.not_equals, operator: AbilityDSLToken_1.AbilityDSLToken.NOT_EQ };
1971
+ }
1972
+ this.pos = savedPos;
1973
+ // is not equals
1974
+ if (this.matchWord('is') && this.matchWord('not') && this.matchWord('equals')) {
1975
+ return { condition: AbilityCondition_1.default.not_equals, operator: AbilityDSLToken_1.AbilityDSLToken.NOT_EQ };
1976
+ }
1977
+ this.pos = savedPos;
1978
+ // is in
1979
+ if (this.matchWord('is') && this.matchWord('in')) {
1980
+ return { condition: AbilityCondition_1.default.in, operator: AbilityDSLToken_1.AbilityDSLToken.IN };
1981
+ }
1982
+ this.pos = savedPos;
1983
+ // not in
1984
+ if (this.matchWord('not') && this.matchWord('in')) {
1985
+ return { condition: AbilityCondition_1.default.not_in, operator: AbilityDSLToken_1.AbilityDSLToken.NOT_IN };
1986
+ }
1987
+ this.pos = savedPos;
1988
+ // is not null
1989
+ if (this.matchWord('is') && this.matchWord('not')) {
1990
+ if (this.check(AbilityDSLToken_1.AbilityDSLToken.NULL)) {
1991
+ this.advance();
1992
+ return {
1993
+ condition: AbilityCondition_1.default.not_equals,
1994
+ operator: AbilityDSLToken_1.AbilityDSLToken.NOT_EQ_NULL,
1995
+ };
1996
+ }
1997
+ }
1998
+ this.pos = savedPos;
1999
+ // is null
2000
+ if (this.matchWord('is') && this.matchWord('null')) {
2001
+ if (this.check(AbilityDSLToken_1.AbilityDSLToken.NULL)) {
2002
+ this.advance();
2003
+ return {
2004
+ condition: AbilityCondition_1.default.equals,
2005
+ operator: AbilityDSLToken_1.AbilityDSLToken.EQ_NULL,
2006
+ };
2007
+ }
2008
+ }
2009
+ this.pos = savedPos;
2010
+ // Single token (symbol or keyword)
2011
+ const token = this.peek();
2012
+ if (token.code !== AbilityDSLToken_1.AbilityDSLToken.SYMBOL &&
2013
+ token.code !== AbilityDSLToken_1.AbilityDSLToken.KEYWORD &&
2014
+ token.code !== AbilityDSLToken_1.AbilityDSLToken.NULL) {
2015
+ this.syntaxError(`Expected comparison operator, got \`${token.value}\``, token, [
2016
+ AbilityDSLToken_1.AbilityDSLToken.SYMBOL,
2017
+ AbilityDSLToken_1.AbilityDSLToken.KEYWORD,
2018
+ AbilityDSLToken_1.AbilityDSLToken.NULL,
2019
+ ]);
2020
+ }
2021
+ this.advance();
2022
+ switch (token.code) {
2023
+ case AbilityDSLToken_1.AbilityDSLToken.SYMBOL:
2024
+ if (token.value === '=' || token.value === '==')
2025
+ return { condition: AbilityCondition_1.default.equals, operator: AbilityDSLToken_1.AbilityDSLToken.EQ };
2026
+ if (token.value === '!=' || token.value === '<>')
2027
+ return { condition: AbilityCondition_1.default.not_equals, operator: AbilityDSLToken_1.AbilityDSLToken.NOT_EQ };
2028
+ if (token.value === '>')
2029
+ return { condition: AbilityCondition_1.default.greater_than, operator: AbilityDSLToken_1.AbilityDSLToken.GT };
2030
+ if (token.value === '<')
2031
+ return { condition: AbilityCondition_1.default.less_than, operator: AbilityDSLToken_1.AbilityDSLToken.LT };
2032
+ if (token.value === '>=')
2033
+ return { condition: AbilityCondition_1.default.greater_or_equal, operator: AbilityDSLToken_1.AbilityDSLToken.GTE };
2034
+ if (token.value === '<=')
2035
+ return { condition: AbilityCondition_1.default.less_or_equal, operator: AbilityDSLToken_1.AbilityDSLToken.LTE };
2036
+ break;
2037
+ case AbilityDSLToken_1.AbilityDSLToken.KEYWORD:
2038
+ if (token.value === 'contains' || token.value === 'includes' || token.value === 'has')
2039
+ return { condition: AbilityCondition_1.default.contains, operator: AbilityDSLToken_1.AbilityDSLToken.CONTAINS };
2040
+ if (token.value === 'in')
2041
+ return { condition: AbilityCondition_1.default.in, operator: AbilityDSLToken_1.AbilityDSLToken.IN };
2042
+ if (token.value === 'equals')
2043
+ return { condition: AbilityCondition_1.default.equals, operator: AbilityDSLToken_1.AbilityDSLToken.EQ };
2044
+ if (token.value === 'gte') {
2045
+ return { condition: AbilityCondition_1.default.greater_or_equal, operator: AbilityDSLToken_1.AbilityDSLToken.GTE };
2046
+ }
2047
+ if (token.value === 'greater' || token.value === 'gt') {
2048
+ // If we come here, it means "greater" without "than" – treat as '>'
2049
+ return { condition: AbilityCondition_1.default.greater_than, operator: AbilityDSLToken_1.AbilityDSLToken.GT };
2050
+ }
2051
+ if (token.value === 'less' || token.value === 'lt') {
2052
+ return { condition: AbilityCondition_1.default.less_than, operator: AbilityDSLToken_1.AbilityDSLToken.LT };
2053
+ }
2054
+ if (token.value === 'lte') {
2055
+ return { condition: AbilityCondition_1.default.less_or_equal, operator: AbilityDSLToken_1.AbilityDSLToken.LTE };
2056
+ }
2057
+ if (token.value === 'is') {
2058
+ // "is" alone -> equals
2059
+ return { condition: AbilityCondition_1.default.equals, operator: AbilityDSLToken_1.AbilityDSLToken.EQ };
2060
+ }
2061
+ break;
2062
+ default:
2063
+ break;
2064
+ }
2065
+ return this.syntaxError(`Unexpected operator token \`${token.value}\``, token, [
2066
+ AbilityDSLToken_1.AbilityDSLToken.SYMBOL,
2067
+ AbilityDSLToken_1.AbilityDSLToken.KEYWORD,
2068
+ ]);
2069
+ }
2070
+ /**
2071
+ * Helper to match and consume a specific word token (KEYWORD or IDENTIFIER).
2072
+ * @param word The exact string to look for.
2073
+ * @returns True if the next token has that value.
2074
+ */
2075
+ matchWord(word) {
2076
+ if (this.isAtEnd()) {
2077
+ return false;
2078
+ }
2079
+ const token = this.peek();
2080
+ if ((token.code === AbilityDSLToken_1.AbilityDSLToken.KEYWORD || token.code === AbilityDSLToken_1.AbilityDSLToken.IDENTIFIER) &&
2081
+ token.value === word) {
2082
+ this.advance();
2083
+ return true;
2084
+ }
2085
+ return false;
2086
+ }
2087
+ matchSymbol(symbol) {
2088
+ if (this.isAtEnd())
2089
+ return false;
2090
+ const token = this.peek();
2091
+ if (token.code === AbilityDSLToken_1.AbilityDSLToken.SYMBOL && token.value === symbol) {
2092
+ this.advance();
2093
+ return true;
2094
+ }
2095
+ return false;
2096
+ }
2097
+ // -------------------------------------------------------------------------
2098
+ // #region Value parsing (literals, arrays, identifiers)
2099
+ // -------------------------------------------------------------------------
2100
+ /**
2101
+ * Parses a resource value. Can be a string literal, number, boolean,
2102
+ * null, a path (identifier), or an array.
2103
+ */
2104
+ parseValue() {
2105
+ // Arrays start with a left bracket
2106
+ if (this.check(AbilityDSLToken_1.AbilityDSLToken.LBRACKET)) {
2107
+ this.advance();
2108
+ return this.parseArray();
2109
+ }
2110
+ // Ensure we are not about to read a structural token as a value.
2111
+ const token = this.peek();
2112
+ if (token.code === AbilityDSLToken_1.AbilityDSLToken.ALL ||
2113
+ token.code === AbilityDSLToken_1.AbilityDSLToken.ANY ||
2114
+ token.code === AbilityDSLToken_1.AbilityDSLToken.EFFECT) {
2115
+ this.syntaxError(`Unexpected ${token.code} in value position`, token);
2116
+ }
2117
+ this.advance();
2118
+ // CHECK THIS SWITCH COMPARE
2119
+ switch (token.code) {
2120
+ case AbilityDSLToken_1.AbilityDSLToken.STRING:
2121
+ return token.value;
2122
+ case AbilityDSLToken_1.AbilityDSLToken.NUMBER:
2123
+ return Number(token.value);
2124
+ case AbilityDSLToken_1.AbilityDSLToken.BOOLEAN:
2125
+ return token.value === 'true';
2126
+ case AbilityDSLToken_1.AbilityDSLToken.NULL:
2127
+ return null;
2128
+ case AbilityDSLToken_1.AbilityDSLToken.IDENTIFIER:
2129
+ return token.value;
2130
+ default: {
2131
+ this.syntaxError(`Unexpected value token "${token.value}"`, token ?? this.tokens[this.tokens.length - 1], [AbilityDSLToken_1.AbilityDSLToken.KEYWORD]);
2132
+ }
2133
+ }
2134
+ }
2135
+ /**
2136
+ * Parses an array literal: [ <value>, <value>, ... ]
2137
+ * The opening bracket has already been consumed.
2138
+ */
2139
+ parseArray() {
2140
+ const arr = [];
2141
+ // Handle empty array
2142
+ if (this.check(AbilityDSLToken_1.AbilityDSLToken.RBRACKET)) {
2143
+ this.advance();
2144
+ return arr;
2145
+ }
2146
+ while (!this.isAtEnd() && !this.check(AbilityDSLToken_1.AbilityDSLToken.RBRACKET)) {
2147
+ const value = this.parseValue();
2148
+ // Flatten nested arrays if they appear (though grammar doesn't currently allow nesting).
2149
+ if (Array.isArray(value)) {
2150
+ arr.push(...value);
2151
+ }
2152
+ else if (typeof value === 'string' ||
2153
+ typeof value === 'number' ||
2154
+ typeof value === 'boolean') {
2155
+ arr.push(value);
2156
+ }
2157
+ else if (value === null) {
2158
+ // Null is allowed in arrays? Currently, we throw.
2159
+ this.syntaxError('Unexpected null in array', this.peek());
2160
+ }
2161
+ // Optional comma between elements
2162
+ if (this.check(AbilityDSLToken_1.AbilityDSLToken.COMMA)) {
2163
+ this.advance();
2164
+ }
2165
+ }
2166
+ this.consume(AbilityDSLToken_1.AbilityDSLToken.RBRACKET, 'Expected "]"');
2167
+ return arr;
2168
+ }
2169
+ // -------------------------------------------------------------------------
2170
+ // #region Annotations and comments
2171
+ // -------------------------------------------------------------------------
2172
+ consumeLeadingComments() {
2173
+ while (this.check(AbilityDSLToken_1.AbilityDSLToken.COMMENT)) {
2174
+ const token = this.advance();
2175
+ this.processCommentToken(token);
2176
+ }
2177
+ }
2178
+ processCommentToken(token) {
2179
+ const text = token.value.trim();
2180
+ if (text.startsWith('@name ')) {
2181
+ this.annotationBuffer.name = text.slice(6).trim();
2182
+ }
2183
+ if (text.startsWith('@description ')) {
2184
+ this.annotationBuffer.description = text.slice(13).trim();
2185
+ }
2186
+ }
2187
+ takeAnnotations() {
2188
+ const meta = { ...this.annotationBuffer };
2189
+ this.annotationBuffer = {
2190
+ name: null,
2191
+ description: null,
2192
+ };
2193
+ return meta;
2194
+ }
2195
+ // -------------------------------------------------------------------------
2196
+ // #region Errors
2197
+ // -------------------------------------------------------------------------
2198
+ syntaxError(details, token, expected) {
2199
+ const lines = this.dsl.split(/\r?\n/);
2200
+ const lineIdx = token.line - 1;
2201
+ const lineBefore = lineIdx > 0 ? lines[lineIdx - 1] : '';
2202
+ const current = lines[lineIdx];
2203
+ const linesAfter = lineIdx + 1 < lines.length ? lines[lineIdx + 1] : '';
2204
+ const wave = ' '.repeat(Math.max(0, token.column - 1)) + '~'.repeat(token.value.length);
2205
+ const lineNumWidth = String(token.line + 1).length;
2206
+ const num = (n) => String(n).padStart(lineNumWidth, ' ');
2207
+ let context = '';
2208
+ if (lineBefore.trim() !== '') {
2209
+ context += `${num(token.line - 1)} | ${lineBefore}\n`;
2210
+ }
2211
+ context += `${num(token.line)} | ${current}\n`;
2212
+ context += `${' '.repeat(lineNumWidth)} | ${wave}\n`;
2213
+ if (linesAfter.trim() !== '') {
2214
+ context += `${num(token.line + 1)} | ${linesAfter}`;
2215
+ }
2216
+ let finalDetails = details;
2217
+ if (expected && expected?.length > 0) {
2218
+ const actual = token.value;
2219
+ const suggestion = this.suggest(actual, expected);
2220
+ const detailsMsg = `${details}\nDetails: Unexpected value token \`${actual}\``;
2221
+ finalDetails = suggestion ? `${detailsMsg} Did you mean \`${suggestion}\`?` : detailsMsg;
2222
+ }
2223
+ throw new AbilityDSLSyntaxError_1.AbilityDSLSyntaxError(token.line, token.column, context + '\n', finalDetails);
2224
+ }
2225
+ getLine(lineNumber) {
2226
+ return this.dsl.split(/\r?\n/)[lineNumber - 1] ?? '';
2227
+ }
2228
+ suggest(actual, expectedTypes) {
2229
+ const candidates = [];
2230
+ for (const type of expectedTypes) {
2231
+ candidates.push(type);
2232
+ }
2233
+ const uniqueCandidates = [...new Set(candidates)];
2234
+ let best = null;
2235
+ let bestDist = 3;
2236
+ for (const candidate of uniqueCandidates) {
2237
+ const d = this.levenshteinDistance(actual.toLowerCase(), candidate.toLowerCase());
2238
+ if (d < bestDist) {
2239
+ bestDist = d;
2240
+ best = candidate;
2241
+ }
2242
+ }
2243
+ return best;
2244
+ }
2245
+ levenshteinDistance(a, b) {
2246
+ const matrix = Array(b.length + 1)
2247
+ .fill(null)
2248
+ .map(() => Array(a.length + 1).fill(null));
2249
+ for (let i = 0; i <= a.length; i++)
2250
+ matrix[0][i] = i;
2251
+ for (let j = 0; j <= b.length; j++)
2252
+ matrix[j][0] = j;
2253
+ for (let j = 1; j <= b.length; j++) {
2254
+ for (let i = 1; i <= a.length; i++) {
2255
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
2256
+ matrix[j][i] = Math.min(matrix[j][i - 1] + 1, matrix[j - 1][i] + 1, matrix[j - 1][i - 1] + cost);
2257
+ }
2258
+ }
2259
+ return matrix[b.length][a.length];
2260
+ }
2261
+ // -------------------------------------------------------------------------
2262
+ // #region Helper / lookahead methods
2263
+ // -------------------------------------------------------------------------
2264
+ consumeOneOf(types, message) {
2265
+ const token = this.peek();
2266
+ for (const t of types) {
2267
+ if (token && token.code === t) {
2268
+ return this.advance();
2269
+ }
2270
+ }
2271
+ const expected = types.map(t => t).join(', ');
2272
+ const actual = token ? token.value : AbilityDSLToken_1.AbilityDSLToken.EOF;
2273
+ const suggestion = this.suggest(actual, types);
2274
+ const details = `${message}\nDetails: Unexpected token \`${actual}\`, expected one of: ${expected}.`;
2275
+ const finalMsg = suggestion ? `${details} Did you mean \`${suggestion}\`?` : details;
2276
+ this.syntaxError(finalMsg, token ?? this.tokens[this.tokens.length - 1]);
2277
+ }
2278
+ consume(type, message) {
2279
+ const token = this.peek();
2280
+ if (token && token.code === type) {
2281
+ return this.advance();
2282
+ }
2283
+ const expected = type;
2284
+ const actual = token ? token.value : AbilityDSLToken_1.AbilityDSLToken.EOF;
2285
+ const suggestion = this.suggest(actual, [type]);
2286
+ const details = `${message}\nDetails: Unexpected token \`${actual}\`, expected "${expected}".`;
2287
+ const finalMsg = suggestion ? `${details} Did you mean \`${suggestion}\`?` : details;
2288
+ this.syntaxError(finalMsg, token ?? this.tokens[this.tokens.length - 1]);
2289
+ }
2290
+ check(type) {
2291
+ if (this.isAtEnd())
2292
+ return false;
2293
+ return this.peek().code === type;
2294
+ }
2295
+ isStartOfPolicy() {
2296
+ return this.check(AbilityDSLToken_1.AbilityDSLToken.EFFECT);
2297
+ }
2298
+ isStartOfGroup() {
2299
+ return this.check(AbilityDSLToken_1.AbilityDSLToken.ALL) || this.check(AbilityDSLToken_1.AbilityDSLToken.ANY);
2300
+ }
2301
+ advance() {
2302
+ return this.tokens[this.pos++];
2303
+ }
2304
+ peek() {
2305
+ return this.tokens[this.pos];
2306
+ }
2307
+ isAtEnd() {
2308
+ return this.peek().code === AbilityDSLToken_1.AbilityDSLToken.EOF;
2309
+ }
2310
+ }
2311
+ exports.AbilityDSLParser = AbilityDSLParser;
2312
+
2313
+
2314
+ /***/ }),
2315
+
2316
+ /***/ 883:
2317
+ /***/ ((__unused_webpack_module, exports) => {
2318
+
2319
+
2320
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
2321
+ exports.AbilityDSLSyntaxError = void 0;
2322
+ class AbilityDSLSyntaxError extends Error {
2323
+ line;
2324
+ column;
2325
+ context;
2326
+ details;
2327
+ _formattedMessage;
2328
+ _originalStack;
2329
+ constructor(line, column, context, // строка DSL + ^ + соседние строки
2330
+ details) {
2331
+ super(details.split('\n')[0]); // message = только первая строка
2332
+ this.line = line;
2333
+ this.column = column;
2334
+ this.context = context;
2335
+ this.details = details;
2336
+ this.name = 'AbilityDSLSyntaxError';
2337
+ if (Error.captureStackTrace) {
2338
+ Error.captureStackTrace(this, AbilityDSLSyntaxError);
2339
+ }
2340
+ this._originalStack = this.stack;
2341
+ this._formattedMessage = this.formatMessage();
2342
+ Object.defineProperty(this, 'stack', {
2343
+ get: () => this._formattedMessage,
2344
+ configurable: true,
2345
+ });
2346
+ }
2347
+ static supportsColor() {
2348
+ return typeof process !== 'undefined' && process.stdout?.isTTY;
2349
+ }
2350
+ formatMessage() {
2351
+ const useColor = AbilityDSLSyntaxError.supportsColor();
2352
+ const BOLD = useColor ? '\x1b[1m' : '';
2353
+ const RED = useColor ? '\x1b[31m' : '';
2354
+ const ORANGE = useColor ? '\x1b[33;1m' : '';
2355
+ const GRAY = useColor ? '\x1b[90m' : '';
2356
+ const RESET = useColor ? '\x1b[0m' : '';
2357
+ const lines = this.context.split('\n');
2358
+ // Find line with ^
2359
+ const pointerIndex = lines.findIndex(l => l.includes('^') || l.includes('~'));
2360
+ const commentIndex = lines.findIndex(l => l.trim().includes('#'));
2361
+ const formattedLines = lines.map((line, idx) => {
2362
+ if (idx === pointerIndex - 1) {
2363
+ // Error line
2364
+ return `${BOLD}${ORANGE}${line}${RESET}`;
2365
+ }
2366
+ if (idx === pointerIndex) {
2367
+ // Error with ~~~~~
2368
+ return `${RED}${line}${RESET}`;
2369
+ }
2370
+ // Comments # ...
2371
+ if (idx === commentIndex) {
2372
+ return `${GRAY}${line}${RESET}`;
2373
+ }
2374
+ return line;
2375
+ });
2376
+ const contextBlock = formattedLines.join('\n');
2377
+ return `${BOLD}${RED}${this.name}: ${this.details}${RESET}\n\n` + contextBlock;
2378
+ }
2379
+ toString() {
2380
+ return this._formattedMessage;
2381
+ }
2382
+ }
2383
+ exports.AbilityDSLSyntaxError = AbilityDSLSyntaxError;
2384
+
2385
+
2386
+ /***/ }),
2387
+
2388
+ /***/ 325:
2389
+ /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
2390
+
2391
+
2392
+ var __importDefault = (this && this.__importDefault) || function (mod) {
2393
+ return (mod && mod.__esModule) ? mod : { "default": mod };
2394
+ };
2395
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
2396
+ exports.AbilityDSLToken = void 0;
2397
+ const AbilityCode_1 = __importDefault(__webpack_require__(301));
2398
+ /**
2399
+ * Represents a single token produced by the Ability DSL lexer.
2400
+ * Each token carries a type (e.g., EFFECT, IDENTIFIER, STRING) and its raw string value.
2401
+ */
2402
+ class AbilityDSLToken extends AbilityCode_1.default {
2403
+ /** The literal text of the token as it appeared in the input (e.g., "permit", "user.roles", "admin"). */
2404
+ value = '';
2405
+ /** The line number in DSL */
2406
+ line = 1;
2407
+ /** The column in dsl */
2408
+ column = 1;
2409
+ constructor(type, value, line, column) {
2410
+ super(type);
2411
+ this.value = value;
2412
+ this.line = line;
2413
+ this.column = column;
2414
+ }
2415
+ /**
2416
+ * Returns a human-readable representation of the token, useful for debugging.
2417
+ * Example output: "AbilityDSLToken([EFFECT] permit"
2418
+ */
2419
+ toString() {
2420
+ return `AbilityDSLToken([${this.code}] "${this.value}" at ${this.line}:${this.column})`;
2421
+ }
2422
+ static EFFECT = 'EFFECT';
2423
+ static IF = 'IF';
2424
+ static PERMISSION = 'PERMISSION';
2425
+ static IDENTIFIER = 'IDENTIFIER';
2426
+ static COLON = 'COLON';
2427
+ static COMMA = 'COMMA';
2428
+ static DOT = 'DOT';
2429
+ static LBRACKET = 'LBRACKET';
2430
+ static RBRACKET = 'RBRACKET';
2431
+ static ALL = 'ALL';
2432
+ static ANY = 'ANY';
2433
+ static OF = 'OF';
2434
+ static EOF = 'EOF';
2435
+ static COMMENT = 'COMMENT';
2436
+ static EQ = 'EQ';
2437
+ static CONTAINS = 'CONTAINS';
2438
+ static IN = 'IN';
2439
+ static NOT_IN = 'NOT_IN';
2440
+ static NOT_CONTAINS = 'NOT_CONTAINS';
2441
+ static GT = 'GT';
2442
+ static GTE = 'GTE';
2443
+ static LT = 'LT';
2444
+ static LTE = 'LTE';
2445
+ static NULL = 'NULL';
2446
+ static EQ_NULL = 'EQ_NULL';
2447
+ static NOT_EQ_NULL = 'NOT_EQ_NULL';
2448
+ static LEN_GT = 'LEN_GT';
2449
+ static LEN_LT = 'LEN_LT';
2450
+ static LEN_EQ = 'LEN_EQ';
2451
+ static NOT_EQ = 'NOT_EQ';
2452
+ static STRING = 'STRING';
2453
+ static NUMBER = 'NUMBER';
2454
+ static BOOLEAN = 'BOOLEAN';
2455
+ static SYMBOL = 'SYMBOL';
2456
+ static KEYWORD = 'KEYWORD';
2457
+ }
2458
+ exports.AbilityDSLToken = AbilityDSLToken;
2459
+
2460
+
2461
+ /***/ }),
2462
+
2463
+ /***/ 909:
2464
+ /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
2465
+
2466
+
2467
+ var __importDefault = (this && this.__importDefault) || function (mod) {
2468
+ return (mod && mod.__esModule) ? mod : { "default": mod };
2469
+ };
2470
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
2471
+ exports.AbilityJSONParser = void 0;
2472
+ const AbilityCondition_1 = __importDefault(__webpack_require__(167));
2473
+ const AbilityRule_1 = __webpack_require__(306);
2474
+ const AbilityRuleSet_1 = __webpack_require__(56);
2475
+ const AbilityCompare_1 = __importDefault(__webpack_require__(413));
2476
+ const AbilityPolicyEffect_1 = __importDefault(__webpack_require__(179));
2477
+ const AbilityPolicy_1 = __webpack_require__(278);
2478
+ class AbilityJSONParser {
2479
+ /**
2480
+ * Parses an array of policy configurations into an array of AbilityPolicy instances.
2481
+ * @param configs - Array of policy configurations
2482
+ * @returns Array of AbilityPolicy instances
2483
+ */
2484
+ static parse(configs) {
2485
+ return configs.map(config => AbilityJSONParser.parsePolicy(config));
2486
+ }
2487
+ static parsePolicy(config) {
2488
+ const { id, name, ruleSet, compareMethod, permission, effect } = config;
2489
+ // Create the empty policy
2490
+ const policy = new AbilityPolicy_1.AbilityPolicy({
2491
+ name,
2492
+ id,
2493
+ permission: permission,
2494
+ effect: new AbilityPolicyEffect_1.default(effect),
2495
+ });
2496
+ policy.compareMethod = new AbilityCompare_1.default(compareMethod);
2497
+ ruleSet.forEach(ruleSetConfig => {
2498
+ policy.addRuleSet(AbilityRuleSet_1.AbilityRuleSet.fromJSON(ruleSetConfig));
2499
+ });
2500
+ return policy;
2501
+ }
2502
+ static parseRule(config) {
2503
+ const { id, name, subject, resource, condition } = config;
2504
+ return new AbilityRule_1.AbilityRule({
2505
+ id,
2506
+ name,
2507
+ subject,
2508
+ resource,
2509
+ condition: new AbilityCondition_1.default(condition),
2510
+ });
2511
+ }
2512
+ /**
2513
+ * Parse the config JSON format to Group class instance
2514
+ */
2515
+ static parseRuleSet(config) {
2516
+ const { id, name, rules, compareMethod } = config;
2517
+ const ruleSet = new AbilityRuleSet_1.AbilityRuleSet({
2518
+ compareMethod: new AbilityCompare_1.default(compareMethod),
2519
+ name,
2520
+ id,
2521
+ });
2522
+ // Adding rules if exists
2523
+ if (rules && rules.length > 0) {
2524
+ const abilityRules = rules.map(ruleConfig => AbilityRule_1.AbilityRule.fromJSON(ruleConfig));
2525
+ ruleSet.addRules(abilityRules);
2526
+ }
2527
+ return ruleSet;
2528
+ }
2529
+ static ruleToJSON(rule) {
2530
+ return {
2531
+ id: rule.id,
2532
+ name: rule.name,
2533
+ subject: rule.subject,
2534
+ resource: rule.resource,
2535
+ condition: rule.condition.code,
2536
+ };
2537
+ }
2538
+ static ruleSetToJSON(ruleSet) {
2539
+ return {
2540
+ id: ruleSet.id.toString(),
2541
+ name: ruleSet.name.toString(),
2542
+ compareMethod: ruleSet.compareMethod.code.toString(),
2543
+ rules: ruleSet.rules.map(rule => AbilityJSONParser.ruleToJSON(rule)),
2544
+ };
2545
+ }
2546
+ static policyToJSON(policy) {
2547
+ return {
2548
+ id: policy.id.toString(),
2549
+ name: policy.name.toString(),
2550
+ compareMethod: policy.compareMethod.code.toString(),
2551
+ ruleSet: policy.ruleSet.map(ruleSet => AbilityJSONParser.ruleSetToJSON(ruleSet)),
2552
+ permission: policy.permission,
2553
+ effect: policy.effect.code,
2554
+ };
2555
+ }
2556
+ static toJSON(policies) {
2557
+ return policies.map(policy => AbilityJSONParser.policyToJSON(policy));
2558
+ }
2559
+ }
2560
+ exports.AbilityJSONParser = AbilityJSONParser;
1097
2561
 
1098
2562
 
1099
2563
  /***/ })