@via-profit/ability 3.6.1 → 3.6.4

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.d.ts CHANGED
@@ -218,14 +218,16 @@ type AbilityExplainConfig = {
218
218
  readonly type: AbilityExplainType;
219
219
  readonly name: string;
220
220
  readonly match: AbilityMatchType;
221
+ readonly debugInfo?: string;
221
222
  };
222
223
  declare class AbilityExplain {
223
224
  readonly type: AbilityExplainType;
224
225
  readonly children: AbilityExplain[];
225
226
  readonly name: string;
226
227
  readonly match: AbilityMatchType;
228
+ readonly debugInfo?: string;
227
229
  constructor(config: AbilityExplainConfig, children?: AbilityExplain[]);
228
- toString(indent?: number): string;
230
+ toString(indentPrefix?: string, isLast?: boolean): string;
229
231
  }
230
232
  declare class AbilityExplainRule extends AbilityExplain {
231
233
  constructor(rule: AbilityRule);
@@ -394,10 +396,40 @@ declare abstract class AbilityStrategy<Resource extends ResourceObject = Record<
394
396
  readonly policies: readonly AbilityPolicy<Resource, Environment>[];
395
397
  private readonly matched;
396
398
  constructor(policies: readonly AbilityPolicy<Resource, Environment>[]);
399
+ /**
400
+ * Executes the strategy’s decision logic and returns the final policy effect
401
+ * (permit or deny).
402
+ *
403
+ * Each concrete strategy must implement its own evaluation rules:
404
+ * - how matched policies are interpreted,
405
+ * - how priority, order, or overrides are applied,
406
+ * - and how the final effect is determined.
407
+ *
408
+ * The implementation must also record the policy that actually determined
409
+ * the outcome (if any), so that decisivePolicy() can later expose it.
410
+ */
397
411
  abstract evaluate(): AbilityPolicyEffectType;
412
+ /**
413
+ * Returns the policy that directly determined the final decision of the strategy.
414
+ *
415
+ * This is:
416
+ * - the specific policy chosen by the strategy’s evaluation rules, or
417
+ * - null if the decision was made by default (e.g., no matched policies,
418
+ * or the strategy cannot identify a single decisive policy).
419
+ *
420
+ * This method performs no computation; it simply exposes the result stored
421
+ * during evaluate(), enabling explainability and debugging tools to show
422
+ * why a decision was made.
423
+ */
424
+ abstract decisivePolicy(): AbilityPolicy<Resource, Environment> | null;
398
425
  matchedPolicies(): readonly AbilityPolicy<Resource, Environment, string>[];
426
+ hasMatched(): boolean;
399
427
  protected firstMatched(): AbilityPolicy<Resource, Environment> | null;
400
428
  protected lastMatched(): AbilityPolicy<Resource, Environment> | null;
429
+ protected firstDenied(): AbilityPolicy<Resource, Environment> | null;
430
+ protected firstPermitted(): AbilityPolicy<Resource, Environment> | null;
431
+ protected getPermitPolicies(): AbilityPolicy<Resource, Environment, string>[];
432
+ protected getDenyPolicies(): AbilityPolicy<Resource, Environment, string>[];
401
433
  protected hasPermit(): boolean;
402
434
  protected hasDeny(): boolean;
403
435
  isAllowed(): boolean;
@@ -414,7 +446,9 @@ declare class AbilityResult<R extends ResourceObject = Record<string, unknown>,
414
446
  *
415
447
  * Useful for debugging, logging, or building UI tools that visualize permission logic.
416
448
  */
417
- explain(): readonly AbilityExplain[];
449
+ explain(): string;
450
+ decisive(): AbilityPolicy<R, E, string> | null;
451
+ explainDecisive(): string | null;
418
452
  isAllowed: () => boolean;
419
453
  isDenied: () => boolean;
420
454
  }
@@ -551,6 +585,7 @@ declare class AbilityDSLParser<R extends ResourceObject = Record<string, unknown
551
585
  private takeAnnotations;
552
586
  private isStartOfPolicy;
553
587
  private isStartOfGroup;
588
+ private isStartOfRule;
554
589
  private isStartOfExcept;
555
590
  private isStartOfAlias;
556
591
  }
@@ -656,7 +691,9 @@ declare function ability<R extends ResourceObject = Record<string, unknown>, E e
656
691
  * Result: deny (because not all policies permitted)
657
692
  */
658
693
  declare class AllMustPermitStrategy<R extends ResourceObject, E extends EnvironmentObject = Record<string, unknown>> extends AbilityStrategy<R, E> {
694
+ private _decisive;
659
695
  evaluate(): AbilityPolicyEffectType;
696
+ decisivePolicy(): AbilityPolicy<R, E, string> | null;
660
697
  }
661
698
 
662
699
  /**
@@ -677,7 +714,9 @@ declare class AllMustPermitStrategy<R extends ResourceObject, E extends Environm
677
714
  * Result: permit (because at least one policy permitted)
678
715
  */
679
716
  declare class AnyPermitStrategy<R extends ResourceObject, E extends EnvironmentObject = Record<string, unknown>> extends AbilityStrategy<R, E> {
717
+ private _decisive;
680
718
  evaluate(): AbilityPolicyEffectType;
719
+ decisivePolicy(): AbilityPolicy<R, E, string> | null;
681
720
  }
682
721
 
683
722
  /**
@@ -699,7 +738,9 @@ declare class AnyPermitStrategy<R extends ResourceObject, E extends EnvironmentO
699
738
  * Result: deny (because deny overrides everything)
700
739
  */
701
740
  declare class DenyOverridesStrategy<R extends ResourceObject, E extends EnvironmentObject = Record<string, unknown>> extends AbilityStrategy<R, E> {
741
+ private _decisive;
702
742
  evaluate(): AbilityPolicyEffectType;
743
+ decisivePolicy(): AbilityPolicy<R, E, string> | null;
703
744
  }
704
745
 
705
746
  /**
@@ -720,7 +761,9 @@ declare class DenyOverridesStrategy<R extends ResourceObject, E extends Environm
720
761
  * Result: permit (P2 is the first applicable)
721
762
  */
722
763
  declare class FirstMatchStrategy<R extends ResourceObject, E extends EnvironmentObject = Record<string, unknown>> extends AbilityStrategy<R, E> {
764
+ private _decisive;
723
765
  evaluate(): AbilityPolicyEffectType;
766
+ decisivePolicy(): AbilityPolicy<R, E, string> | null;
724
767
  }
725
768
 
726
769
  /**
@@ -740,7 +783,9 @@ declare class FirstMatchStrategy<R extends ResourceObject, E extends Environment
740
783
  * Result: deny (more than one applicable policy)
741
784
  */
742
785
  declare class OnlyOneApplicableStrategy<R extends ResourceObject, E extends EnvironmentObject = Record<string, unknown>> extends AbilityStrategy<R, E> {
786
+ private _decisive;
743
787
  evaluate(): AbilityPolicyEffectType;
788
+ decisivePolicy(): AbilityPolicy<R, E, string> | null;
744
789
  }
745
790
 
746
791
  /**
@@ -762,7 +807,9 @@ declare class OnlyOneApplicableStrategy<R extends ResourceObject, E extends Envi
762
807
  * Result: permit (permit overrides deny)
763
808
  */
764
809
  declare class PermitOverridesStrategy<R extends ResourceObject, E extends EnvironmentObject = Record<string, unknown>> extends AbilityStrategy<R, E> {
810
+ private _decisive;
765
811
  evaluate(): AbilityPolicyEffectType;
812
+ decisivePolicy(): AbilityPolicy<R, E, string> | null;
766
813
  }
767
814
 
768
815
  /**
@@ -782,7 +829,9 @@ declare class PermitOverridesStrategy<R extends ResourceObject, E extends Enviro
782
829
  * Result: permit (P3 is the last applicable)
783
830
  */
784
831
  declare class SequentialLastMatchStrategy<R extends ResourceObject, E extends EnvironmentObject = Record<string, unknown>> extends AbilityStrategy<R, E> {
832
+ private _decisive;
785
833
  evaluate(): AbilityPolicyEffectType;
834
+ decisivePolicy(): AbilityPolicy<R, E, string> | null;
786
835
  }
787
836
 
788
837
  /**
@@ -803,7 +852,9 @@ declare class SequentialLastMatchStrategy<R extends ResourceObject, E extends En
803
852
  * Result: permit (P2 has higher priority)
804
853
  */
805
854
  declare class PriorityStrategy<R extends ResourceObject, E extends EnvironmentObject = Record<string, unknown>> extends AbilityStrategy<R, E> {
855
+ private _decisive;
806
856
  evaluate(): AbilityPolicyEffectType;
857
+ decisivePolicy(): AbilityPolicy<R, E, string> | null;
807
858
  }
808
859
 
809
860
  export { AbilityCompare, AbilityCondition, AbilityDSLLexer, AbilityDSLParser, AbilityDSLToken, AbilityError, AbilityExplain, AbilityExplainPolicy, AbilityExplainRule, AbilityExplainRuleSet, AbilityJSONParser, AbilityMatch, AbilityParserError, AbilityPolicy, AbilityPolicyEffect, AbilityResolver, AbilityResult, AbilityRule, AbilityRuleSet, AbilityStrategy, AbilityTypeGenerator, AllMustPermitStrategy, AnyPermitStrategy, DenyOverridesStrategy, FirstMatchStrategy, OnlyOneApplicableStrategy, PermitOverridesStrategy, PriorityStrategy, SequentialLastMatchStrategy, TokenTypes, ability, fromLiteral, isConditionEqual, isConditionNotEqual, toLiteral };
package/dist/index.js CHANGED
@@ -126,27 +126,53 @@ const AbilityMatch = {
126
126
  disabled: brand$2('disabled'),
127
127
  };
128
128
 
129
+ const colors = {
130
+ reset: '\x1b[0m',
131
+ green: '\x1b[32m',
132
+ red: '\x1b[31m',
133
+ blue: '\x1b[34m',
134
+ yellow: '\x1b[33m',
135
+ white: '\x1b[37m',
136
+ gray: '\x1b[90m',
137
+ };
129
138
  class AbilityExplain {
130
139
  type;
131
140
  children;
132
141
  name;
133
142
  match;
143
+ debugInfo;
134
144
  constructor(config, children = []) {
135
145
  this.type = config.type;
136
146
  this.children = children;
137
147
  this.name = config.name;
138
148
  this.match = config.match;
139
- }
140
- toString(indent = 0) {
141
- const pad = ' '.repeat(indent);
142
- const mark = this.match === AbilityMatch.match ? '✓' : '✗';
143
- let out = '';
144
- if (this.type === 'policy') {
145
- out += '\n';
149
+ this.debugInfo = config.debugInfo;
150
+ }
151
+ toString(indentPrefix = '', isLast = true) {
152
+ const isMatch = this.match === AbilityMatch.match;
153
+ const mark = isMatch ? `${colors.green}✓${colors.reset}` : `${colors.red}✗${colors.reset}`;
154
+ let label;
155
+ switch (this.type) {
156
+ case 'policy':
157
+ label = `${colors.blue}POLICY${colors.reset}`;
158
+ break;
159
+ case 'ruleSet':
160
+ label = `${colors.yellow}RULESET${colors.reset}`;
161
+ break;
162
+ default:
163
+ label = `${colors.white}RULE${colors.reset}`;
146
164
  }
147
- out += `${pad}${mark} ${this.type} «${this.name}» is ${this.match}`;
148
- this.children.forEach(child => {
149
- out += '\n' + child.toString(indent + 1);
165
+ const branch = indentPrefix.length === 0
166
+ ? ''
167
+ : isLast
168
+ ? `${colors.gray}└─${colors.reset} `
169
+ : `${colors.gray}├─${colors.reset} `;
170
+ let out = `${indentPrefix}${branch}${label} ${this.name} — ${mark}`;
171
+ if (this.debugInfo)
172
+ out += ` ${colors.gray}(${this.debugInfo})${colors.reset}`;
173
+ const nextIndent = indentPrefix + (isLast ? ' ' : `${colors.gray}│ ${colors.reset}`);
174
+ this.children.forEach((child, idx) => {
175
+ out += '\n' + child.toString(nextIndent, idx === this.children.length - 1);
150
176
  });
151
177
  return out;
152
178
  }
@@ -195,9 +221,21 @@ class AbilityResult {
195
221
  * Useful for debugging, logging, or building UI tools that visualize permission logic.
196
222
  */
197
223
  explain() {
198
- return this.strategy.policies.map(policy => {
199
- return new AbilityExplainPolicy(policy);
200
- });
224
+ return this.strategy.policies
225
+ .map(policy => {
226
+ return new AbilityExplainPolicy(policy).toString();
227
+ })
228
+ .join('\n');
229
+ }
230
+ decisive() {
231
+ return this.strategy.decisivePolicy();
232
+ }
233
+ explainDecisive() {
234
+ const policy = this.decisive();
235
+ if (!policy) {
236
+ return null;
237
+ }
238
+ return new AbilityExplainPolicy(policy).toString();
201
239
  }
202
240
  isAllowed = () => {
203
241
  return this.strategy.isAllowed();
@@ -287,24 +325,25 @@ class AbilityResolver {
287
325
  .toLowerCase(); // optional: make case-insensitive
288
326
  }
289
327
  static matchPermissions(policySegments, inputSegments) {
290
- const maxLen = Math.max(policySegments.length, inputSegments.length);
291
- for (let i = 0; i < maxLen; i++) {
328
+ let i = 0;
329
+ for (; i < policySegments.length; i++) {
292
330
  const pSeg = policySegments[i];
293
331
  const iSeg = inputSegments[i];
294
- if (pSeg === undefined) {
295
- return false;
296
- }
332
+ // '*' глобальный wildcard: матчим всё, что дальше
297
333
  if (pSeg === '*') {
298
- continue; // '*'
334
+ return true;
299
335
  }
336
+ // input закончился раньше — mismatch
300
337
  if (iSeg === undefined) {
301
338
  return false;
302
339
  }
340
+ // обычное сравнение
303
341
  if (pSeg !== iSeg) {
304
342
  return false;
305
343
  }
306
344
  }
307
- return true;
345
+ // Если политика закончилась, но input длиннее — match только если последний сегмент был '*'
346
+ return i === inputSegments.length;
308
347
  }
309
348
  }
310
349
 
@@ -1500,32 +1539,32 @@ class AbilityJSONParser {
1500
1539
  return {
1501
1540
  id: rule.id,
1502
1541
  name: rule.name,
1542
+ disabled: rule.disabled,
1503
1543
  subject: rule.subject,
1504
1544
  resource: rule.resource,
1505
1545
  condition: rule.condition,
1506
- disabled: rule.disabled,
1507
1546
  };
1508
1547
  }
1509
1548
  static ruleSetToJSON(ruleSet) {
1510
1549
  return {
1511
1550
  id: ruleSet.id.toString(),
1512
1551
  name: ruleSet.name.toString(),
1552
+ disabled: ruleSet.disabled,
1513
1553
  compareMethod: ruleSet.compareMethod,
1514
1554
  rules: ruleSet.rules.map(rule => AbilityJSONParser.ruleToJSON(rule)),
1515
- disabled: ruleSet.disabled,
1516
1555
  };
1517
1556
  }
1518
1557
  static policyToJSON(policy) {
1519
1558
  return {
1520
1559
  id: policy.id.toString(),
1521
1560
  name: policy.name.toString(),
1522
- compareMethod: policy.compareMethod,
1523
- ruleSet: policy.ruleSet.map(ruleSet => AbilityJSONParser.ruleSetToJSON(ruleSet)),
1561
+ disabled: policy.disabled,
1562
+ priority: policy.priority,
1524
1563
  permission: policy.permission,
1525
1564
  effect: policy.effect,
1526
- priority: policy.priority,
1527
- disabled: policy.disabled,
1565
+ compareMethod: policy.compareMethod,
1528
1566
  tags: policy.tags,
1567
+ ruleSet: policy.ruleSet.map(ruleSet => AbilityJSONParser.ruleSetToJSON(ruleSet)),
1529
1568
  };
1530
1569
  }
1531
1570
  static toJSON(policies) {
@@ -2313,28 +2352,34 @@ class AbilityDSLParser {
2313
2352
  */
2314
2353
  parseRuleSets(policyCompareMethod) {
2315
2354
  const sets = [];
2355
+ this.consumeLeadingComments();
2356
+ this.consumeLeadingAnnotations();
2316
2357
  while (!this.stream.eof() && !this.isStartOfPolicy()) {
2317
- this.consumeLeadingComments();
2318
- this.consumeLeadingAnnotations();
2319
- // Если начинается новая except группа — парсим её
2358
+ // maybe except ruleSet
2320
2359
  if (this.isStartOfExcept()) {
2321
2360
  sets.push(this.parseExceptGroup(policyCompareMethod));
2322
2361
  continue;
2323
2362
  }
2324
- // Если начинается новая группа — парсим её
2363
+ // maybe ruleSet
2325
2364
  if (this.isStartOfGroup()) {
2326
2365
  sets.push(this.parseGroup());
2327
2366
  continue;
2328
2367
  }
2329
- const annotation = this.takeAnnotations('ruleSet');
2368
+ // implicit ruleSet
2369
+ // if (!this.isStartOfRule()) {
2370
+ // this.consumeLeadingComments();
2371
+ // this.consumeLeadingAnnotations();
2372
+ // }
2373
+ // is implicit group
2374
+ // const annotation = this.takeAnnotations('ruleSet');
2330
2375
  const group = new AbilityRuleSet({
2331
- id: annotation.id?.value || null,
2376
+ // id: annotation.id?.value || null,
2332
2377
  compareMethod: policyCompareMethod,
2333
- name: annotation.name?.value ?? null,
2334
- description: annotation.description?.value || null,
2335
- disabled: annotation.disabled?.value ?? undefined,
2378
+ // name: annotation.name?.value ?? null,
2379
+ // description: annotation.description?.value || null,
2380
+ // disabled: annotation.disabled?.value ?? undefined,
2336
2381
  });
2337
- // Читаем правила implicit-группы
2382
+ // Read rules of implicit-группы
2338
2383
  while (!this.stream.eof()) {
2339
2384
  this.consumeLeadingComments();
2340
2385
  this.consumeLeadingAnnotations();
@@ -2935,6 +2980,11 @@ class AbilityDSLParser {
2935
2980
  isStartOfGroup() {
2936
2981
  return this.stream.check(TokenTypes.ALL) || this.stream.check(TokenTypes.ANY);
2937
2982
  }
2983
+ isStartOfRule() {
2984
+ return (this.stream.check(TokenTypes.IDENTIFIER) ||
2985
+ this.stream.check(TokenTypes.ALWAYS) ||
2986
+ this.stream.check(TokenTypes.NEVER));
2987
+ }
2938
2988
  isStartOfExcept() {
2939
2989
  return this.stream.check(TokenTypes.EXCEPT);
2940
2990
  }
@@ -2958,17 +3008,33 @@ class AbilityStrategy {
2958
3008
  matchedPolicies() {
2959
3009
  return this.matched;
2960
3010
  }
3011
+ hasMatched() {
3012
+ return this.matched.length > 0;
3013
+ }
2961
3014
  firstMatched() {
2962
- return this.matched[0] ?? null;
3015
+ return this.matchedPolicies()[0] ?? null;
2963
3016
  }
2964
3017
  lastMatched() {
2965
- return this.matched.length > 0 ? this.matched[this.matched.length - 1] : null;
3018
+ const list = this.matchedPolicies();
3019
+ return list.length > 0 ? list[list.length - 1] : null;
3020
+ }
3021
+ firstDenied() {
3022
+ return this.getDenyPolicies()[0] ?? null;
3023
+ }
3024
+ firstPermitted() {
3025
+ return this.getPermitPolicies()[0] ?? null;
3026
+ }
3027
+ getPermitPolicies() {
3028
+ return this.matched.filter(p => p.effect === AbilityPolicyEffect.permit);
3029
+ }
3030
+ getDenyPolicies() {
3031
+ return this.matched.filter(p => p.effect === AbilityPolicyEffect.deny);
2966
3032
  }
2967
3033
  hasPermit() {
2968
- return this.matched.some(p => p.effect === AbilityPolicyEffect.permit);
3034
+ return this.getPermitPolicies().length > 0;
2969
3035
  }
2970
3036
  hasDeny() {
2971
- return this.matched.some(p => p.effect === AbilityPolicyEffect.deny);
3037
+ return this.getDenyPolicies().length > 0;
2972
3038
  }
2973
3039
  isAllowed() {
2974
3040
  return this.evaluate() === AbilityPolicyEffect.permit;
@@ -2996,13 +3062,24 @@ class AbilityStrategy {
2996
3062
  * Result: deny (because not all policies permitted)
2997
3063
  */
2998
3064
  class AllMustPermitStrategy extends AbilityStrategy {
3065
+ _decisive = null;
2999
3066
  evaluate() {
3000
- const matched = this.matchedPolicies();
3001
- if (matched.length === 0) {
3067
+ // 1. Нет совпавших политик → deny, но решающей политики нет
3068
+ if (!this.hasMatched()) {
3069
+ this._decisive = null;
3070
+ return AbilityPolicyEffect.deny;
3071
+ }
3072
+ // 2. Если есть deny — она решающая
3073
+ const deny = this.firstDenied();
3074
+ if (deny) {
3075
+ this._decisive = deny;
3002
3076
  return AbilityPolicyEffect.deny;
3003
3077
  }
3004
- const allPermit = matched.every(p => p.effect === AbilityPolicyEffect.permit);
3005
- return allPermit ? AbilityPolicyEffect.permit : AbilityPolicyEffect.deny;
3078
+ this._decisive = this.firstPermitted();
3079
+ return AbilityPolicyEffect.permit;
3080
+ }
3081
+ decisivePolicy() {
3082
+ return this._decisive;
3006
3083
  }
3007
3084
  }
3008
3085
 
@@ -3024,8 +3101,20 @@ class AllMustPermitStrategy extends AbilityStrategy {
3024
3101
  * Result: permit (because at least one policy permitted)
3025
3102
  */
3026
3103
  class AnyPermitStrategy extends AbilityStrategy {
3104
+ _decisive = null;
3027
3105
  evaluate() {
3028
- return this.hasPermit() ? AbilityPolicyEffect.permit : AbilityPolicyEffect.deny;
3106
+ // 1. Если есть permit он решающий
3107
+ const permit = this.firstPermitted();
3108
+ if (permit) {
3109
+ this._decisive = permit;
3110
+ return AbilityPolicyEffect.permit;
3111
+ }
3112
+ // 2. Нет permit → deny по умолчанию
3113
+ this._decisive = null;
3114
+ return AbilityPolicyEffect.deny;
3115
+ }
3116
+ decisivePolicy() {
3117
+ return this._decisive;
3029
3118
  }
3030
3119
  }
3031
3120
 
@@ -3048,15 +3137,27 @@ class AnyPermitStrategy extends AbilityStrategy {
3048
3137
  * Result: deny (because deny overrides everything)
3049
3138
  */
3050
3139
  class DenyOverridesStrategy extends AbilityStrategy {
3140
+ _decisive = null;
3051
3141
  evaluate() {
3052
- if (this.hasDeny()) {
3142
+ // 1. Если есть deny — он решающий
3143
+ const deny = this.firstDenied();
3144
+ if (deny) {
3145
+ this._decisive = deny;
3053
3146
  return AbilityPolicyEffect.deny;
3054
3147
  }
3055
- if (this.hasPermit()) {
3148
+ // 2. Если есть permit — он решающий
3149
+ const permit = this.firstPermitted();
3150
+ if (permit) {
3151
+ this._decisive = permit;
3056
3152
  return AbilityPolicyEffect.permit;
3057
3153
  }
3154
+ // 3. Нет ни permit, ни deny → deny по умолчанию
3155
+ this._decisive = null;
3058
3156
  return AbilityPolicyEffect.deny;
3059
3157
  }
3158
+ decisivePolicy() {
3159
+ return this._decisive;
3160
+ }
3060
3161
  }
3061
3162
 
3062
3163
  /**
@@ -3077,9 +3178,20 @@ class DenyOverridesStrategy extends AbilityStrategy {
3077
3178
  * Result: permit (P2 is the first applicable)
3078
3179
  */
3079
3180
  class FirstMatchStrategy extends AbilityStrategy {
3181
+ _decisive = null;
3080
3182
  evaluate() {
3081
3183
  const first = this.firstMatched();
3082
- return first?.effect ?? AbilityPolicyEffect.deny;
3184
+ // Если нет совпавших политик → deny по умолчанию
3185
+ if (!first) {
3186
+ this._decisive = null;
3187
+ return AbilityPolicyEffect.deny;
3188
+ }
3189
+ // Первая совпавшая политика — решающая
3190
+ this._decisive = first;
3191
+ return first.effect;
3192
+ }
3193
+ decisivePolicy() {
3194
+ return this._decisive;
3083
3195
  }
3084
3196
  }
3085
3197
 
@@ -3100,13 +3212,21 @@ class FirstMatchStrategy extends AbilityStrategy {
3100
3212
  * Result: deny (more than one applicable policy)
3101
3213
  */
3102
3214
  class OnlyOneApplicableStrategy extends AbilityStrategy {
3215
+ _decisive = null;
3103
3216
  evaluate() {
3104
3217
  const matched = this.matchedPolicies();
3218
+ // 1. Ровно одна совпавшая политика → она решающая
3105
3219
  if (matched.length === 1) {
3220
+ this._decisive = matched[0];
3106
3221
  return matched[0].effect;
3107
3222
  }
3223
+ // 2. Иначе deny, решающей политики нет
3224
+ this._decisive = null;
3108
3225
  return AbilityPolicyEffect.deny;
3109
3226
  }
3227
+ decisivePolicy() {
3228
+ return this._decisive;
3229
+ }
3110
3230
  }
3111
3231
 
3112
3232
  /**
@@ -3128,15 +3248,27 @@ class OnlyOneApplicableStrategy extends AbilityStrategy {
3128
3248
  * Result: permit (permit overrides deny)
3129
3249
  */
3130
3250
  class PermitOverridesStrategy extends AbilityStrategy {
3251
+ _decisive = null;
3131
3252
  evaluate() {
3132
- if (this.hasPermit()) {
3253
+ // 1. Если есть permit — он выигрывает
3254
+ const permit = this.matchedPolicies().find(p => p.effect === AbilityPolicyEffect.permit);
3255
+ if (permit) {
3256
+ this._decisive = permit;
3133
3257
  return AbilityPolicyEffect.permit;
3134
3258
  }
3135
- if (this.hasDeny()) {
3259
+ // 2. Если permit нет — ищем deny
3260
+ const deny = this.matchedPolicies().find(p => p.effect === AbilityPolicyEffect.deny);
3261
+ if (deny) {
3262
+ this._decisive = deny;
3136
3263
  return AbilityPolicyEffect.deny;
3137
3264
  }
3265
+ // 3. Нет ни permit, ни deny → deny по умолчанию
3266
+ this._decisive = null;
3138
3267
  return AbilityPolicyEffect.deny;
3139
3268
  }
3269
+ decisivePolicy() {
3270
+ return this._decisive;
3271
+ }
3140
3272
  }
3141
3273
 
3142
3274
  /**
@@ -3156,9 +3288,20 @@ class PermitOverridesStrategy extends AbilityStrategy {
3156
3288
  * Result: permit (P3 is the last applicable)
3157
3289
  */
3158
3290
  class SequentialLastMatchStrategy extends AbilityStrategy {
3291
+ _decisive = null;
3159
3292
  evaluate() {
3160
3293
  const last = this.lastMatched();
3161
- return last?.effect ?? AbilityPolicyEffect.deny;
3294
+ // Нет совпавших политик → deny по умолчанию
3295
+ if (!last) {
3296
+ this._decisive = null;
3297
+ return AbilityPolicyEffect.deny;
3298
+ }
3299
+ // Последняя совпавшая политика — решающая
3300
+ this._decisive = last;
3301
+ return last.effect;
3302
+ }
3303
+ decisivePolicy() {
3304
+ return this._decisive;
3162
3305
  }
3163
3306
  }
3164
3307
 
@@ -3180,13 +3323,23 @@ class SequentialLastMatchStrategy extends AbilityStrategy {
3180
3323
  * Result: permit (P2 has higher priority)
3181
3324
  */
3182
3325
  class PriorityStrategy extends AbilityStrategy {
3326
+ _decisive = null;
3183
3327
  evaluate() {
3184
3328
  const matched = this.matchedPolicies();
3329
+ // 1. Нет совпавших политик → deny, решающей политики нет
3185
3330
  if (matched.length === 0) {
3331
+ this._decisive = null;
3186
3332
  return AbilityPolicyEffect.deny;
3187
3333
  }
3334
+ // 2. Сортируем по приоритету (больший приоритет — выше)
3188
3335
  const sorted = [...matched].sort((a, b) => b.priority - a.priority);
3189
- return sorted[0].effect;
3336
+ // 3. Самая приоритетная политика — решающая
3337
+ const top = sorted[0];
3338
+ this._decisive = top;
3339
+ return top.effect;
3340
+ }
3341
+ decisivePolicy() {
3342
+ return this._decisive;
3190
3343
  }
3191
3344
  }
3192
3345
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@via-profit/ability",
3
3
  "support": "https://via-profit.ru",
4
- "version": "3.6.1",
4
+ "version": "3.6.4",
5
5
  "description": "Via-Profit Ability service",
6
6
  "keywords": [
7
7
  "ability",