@via-profit/ability 3.6.2 → 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,14 @@ type AbilityExplainConfig = {
218
218
  readonly type: AbilityExplainType;
219
219
  readonly name: string;
220
220
  readonly match: AbilityMatchType;
221
- readonly debugInfo?: boolean;
221
+ readonly debugInfo?: string;
222
222
  };
223
223
  declare class AbilityExplain {
224
224
  readonly type: AbilityExplainType;
225
225
  readonly children: AbilityExplain[];
226
226
  readonly name: string;
227
227
  readonly match: AbilityMatchType;
228
- readonly debugInfo?: boolean;
228
+ readonly debugInfo?: string;
229
229
  constructor(config: AbilityExplainConfig, children?: AbilityExplain[]);
230
230
  toString(indentPrefix?: string, isLast?: boolean): string;
231
231
  }
@@ -396,10 +396,40 @@ declare abstract class AbilityStrategy<Resource extends ResourceObject = Record<
396
396
  readonly policies: readonly AbilityPolicy<Resource, Environment>[];
397
397
  private readonly matched;
398
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
+ */
399
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;
400
425
  matchedPolicies(): readonly AbilityPolicy<Resource, Environment, string>[];
426
+ hasMatched(): boolean;
401
427
  protected firstMatched(): AbilityPolicy<Resource, Environment> | null;
402
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>[];
403
433
  protected hasPermit(): boolean;
404
434
  protected hasDeny(): boolean;
405
435
  isAllowed(): boolean;
@@ -416,7 +446,9 @@ declare class AbilityResult<R extends ResourceObject = Record<string, unknown>,
416
446
  *
417
447
  * Useful for debugging, logging, or building UI tools that visualize permission logic.
418
448
  */
419
- explain(): readonly AbilityExplain[];
449
+ explain(): string;
450
+ decisive(): AbilityPolicy<R, E, string> | null;
451
+ explainDecisive(): string | null;
420
452
  isAllowed: () => boolean;
421
453
  isDenied: () => boolean;
422
454
  }
@@ -659,7 +691,9 @@ declare function ability<R extends ResourceObject = Record<string, unknown>, E e
659
691
  * Result: deny (because not all policies permitted)
660
692
  */
661
693
  declare class AllMustPermitStrategy<R extends ResourceObject, E extends EnvironmentObject = Record<string, unknown>> extends AbilityStrategy<R, E> {
694
+ private _decisive;
662
695
  evaluate(): AbilityPolicyEffectType;
696
+ decisivePolicy(): AbilityPolicy<R, E, string> | null;
663
697
  }
664
698
 
665
699
  /**
@@ -680,7 +714,9 @@ declare class AllMustPermitStrategy<R extends ResourceObject, E extends Environm
680
714
  * Result: permit (because at least one policy permitted)
681
715
  */
682
716
  declare class AnyPermitStrategy<R extends ResourceObject, E extends EnvironmentObject = Record<string, unknown>> extends AbilityStrategy<R, E> {
717
+ private _decisive;
683
718
  evaluate(): AbilityPolicyEffectType;
719
+ decisivePolicy(): AbilityPolicy<R, E, string> | null;
684
720
  }
685
721
 
686
722
  /**
@@ -702,7 +738,9 @@ declare class AnyPermitStrategy<R extends ResourceObject, E extends EnvironmentO
702
738
  * Result: deny (because deny overrides everything)
703
739
  */
704
740
  declare class DenyOverridesStrategy<R extends ResourceObject, E extends EnvironmentObject = Record<string, unknown>> extends AbilityStrategy<R, E> {
741
+ private _decisive;
705
742
  evaluate(): AbilityPolicyEffectType;
743
+ decisivePolicy(): AbilityPolicy<R, E, string> | null;
706
744
  }
707
745
 
708
746
  /**
@@ -723,7 +761,9 @@ declare class DenyOverridesStrategy<R extends ResourceObject, E extends Environm
723
761
  * Result: permit (P2 is the first applicable)
724
762
  */
725
763
  declare class FirstMatchStrategy<R extends ResourceObject, E extends EnvironmentObject = Record<string, unknown>> extends AbilityStrategy<R, E> {
764
+ private _decisive;
726
765
  evaluate(): AbilityPolicyEffectType;
766
+ decisivePolicy(): AbilityPolicy<R, E, string> | null;
727
767
  }
728
768
 
729
769
  /**
@@ -743,7 +783,9 @@ declare class FirstMatchStrategy<R extends ResourceObject, E extends Environment
743
783
  * Result: deny (more than one applicable policy)
744
784
  */
745
785
  declare class OnlyOneApplicableStrategy<R extends ResourceObject, E extends EnvironmentObject = Record<string, unknown>> extends AbilityStrategy<R, E> {
786
+ private _decisive;
746
787
  evaluate(): AbilityPolicyEffectType;
788
+ decisivePolicy(): AbilityPolicy<R, E, string> | null;
747
789
  }
748
790
 
749
791
  /**
@@ -765,7 +807,9 @@ declare class OnlyOneApplicableStrategy<R extends ResourceObject, E extends Envi
765
807
  * Result: permit (permit overrides deny)
766
808
  */
767
809
  declare class PermitOverridesStrategy<R extends ResourceObject, E extends EnvironmentObject = Record<string, unknown>> extends AbilityStrategy<R, E> {
810
+ private _decisive;
768
811
  evaluate(): AbilityPolicyEffectType;
812
+ decisivePolicy(): AbilityPolicy<R, E, string> | null;
769
813
  }
770
814
 
771
815
  /**
@@ -785,7 +829,9 @@ declare class PermitOverridesStrategy<R extends ResourceObject, E extends Enviro
785
829
  * Result: permit (P3 is the last applicable)
786
830
  */
787
831
  declare class SequentialLastMatchStrategy<R extends ResourceObject, E extends EnvironmentObject = Record<string, unknown>> extends AbilityStrategy<R, E> {
832
+ private _decisive;
788
833
  evaluate(): AbilityPolicyEffectType;
834
+ decisivePolicy(): AbilityPolicy<R, E, string> | null;
789
835
  }
790
836
 
791
837
  /**
@@ -806,7 +852,9 @@ declare class SequentialLastMatchStrategy<R extends ResourceObject, E extends En
806
852
  * Result: permit (P2 has higher priority)
807
853
  */
808
854
  declare class PriorityStrategy<R extends ResourceObject, E extends EnvironmentObject = Record<string, unknown>> extends AbilityStrategy<R, E> {
855
+ private _decisive;
809
856
  evaluate(): AbilityPolicyEffectType;
857
+ decisivePolicy(): AbilityPolicy<R, E, string> | null;
810
858
  }
811
859
 
812
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
@@ -151,24 +151,28 @@ class AbilityExplain {
151
151
  toString(indentPrefix = '', isLast = true) {
152
152
  const isMatch = this.match === AbilityMatch.match;
153
153
  const mark = isMatch ? `${colors.green}✓${colors.reset}` : `${colors.red}✗${colors.reset}`;
154
- const label = this.type === 'policy'
155
- ? `${colors.blue}POLICY${colors.reset}`
156
- : this.type === 'ruleSet'
157
- ? `${colors.yellow}RULESET${colors.reset}`
158
- : `${colors.white}RULE${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}`;
164
+ }
159
165
  const branch = indentPrefix.length === 0
160
166
  ? ''
161
167
  : isLast
162
168
  ? `${colors.gray}└─${colors.reset} `
163
169
  : `${colors.gray}├─${colors.reset} `;
164
170
  let out = `${indentPrefix}${branch}${label} ${this.name} — ${mark}`;
165
- if (this.debugInfo) {
171
+ if (this.debugInfo)
166
172
  out += ` ${colors.gray}(${this.debugInfo})${colors.reset}`;
167
- }
168
173
  const nextIndent = indentPrefix + (isLast ? ' ' : `${colors.gray}│ ${colors.reset}`);
169
- this.children.forEach((child, index) => {
170
- const last = index === this.children.length - 1;
171
- out += '\n' + child.toString(nextIndent, last);
174
+ this.children.forEach((child, idx) => {
175
+ out += '\n' + child.toString(nextIndent, idx === this.children.length - 1);
172
176
  });
173
177
  return out;
174
178
  }
@@ -217,9 +221,21 @@ class AbilityResult {
217
221
  * Useful for debugging, logging, or building UI tools that visualize permission logic.
218
222
  */
219
223
  explain() {
220
- return this.strategy.policies.map(policy => {
221
- return new AbilityExplainPolicy(policy);
222
- });
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();
223
239
  }
224
240
  isAllowed = () => {
225
241
  return this.strategy.isAllowed();
@@ -309,24 +325,25 @@ class AbilityResolver {
309
325
  .toLowerCase(); // optional: make case-insensitive
310
326
  }
311
327
  static matchPermissions(policySegments, inputSegments) {
312
- const maxLen = Math.max(policySegments.length, inputSegments.length);
313
- for (let i = 0; i < maxLen; i++) {
328
+ let i = 0;
329
+ for (; i < policySegments.length; i++) {
314
330
  const pSeg = policySegments[i];
315
331
  const iSeg = inputSegments[i];
316
- if (pSeg === undefined) {
317
- return false;
318
- }
332
+ // '*' глобальный wildcard: матчим всё, что дальше
319
333
  if (pSeg === '*') {
320
- continue; // '*'
334
+ return true;
321
335
  }
336
+ // input закончился раньше — mismatch
322
337
  if (iSeg === undefined) {
323
338
  return false;
324
339
  }
340
+ // обычное сравнение
325
341
  if (pSeg !== iSeg) {
326
342
  return false;
327
343
  }
328
344
  }
329
- return true;
345
+ // Если политика закончилась, но input длиннее — match только если последний сегмент был '*'
346
+ return i === inputSegments.length;
330
347
  }
331
348
  }
332
349
 
@@ -2991,17 +3008,33 @@ class AbilityStrategy {
2991
3008
  matchedPolicies() {
2992
3009
  return this.matched;
2993
3010
  }
3011
+ hasMatched() {
3012
+ return this.matched.length > 0;
3013
+ }
2994
3014
  firstMatched() {
2995
- return this.matched[0] ?? null;
3015
+ return this.matchedPolicies()[0] ?? null;
2996
3016
  }
2997
3017
  lastMatched() {
2998
- 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);
2999
3032
  }
3000
3033
  hasPermit() {
3001
- return this.matched.some(p => p.effect === AbilityPolicyEffect.permit);
3034
+ return this.getPermitPolicies().length > 0;
3002
3035
  }
3003
3036
  hasDeny() {
3004
- return this.matched.some(p => p.effect === AbilityPolicyEffect.deny);
3037
+ return this.getDenyPolicies().length > 0;
3005
3038
  }
3006
3039
  isAllowed() {
3007
3040
  return this.evaluate() === AbilityPolicyEffect.permit;
@@ -3029,13 +3062,24 @@ class AbilityStrategy {
3029
3062
  * Result: deny (because not all policies permitted)
3030
3063
  */
3031
3064
  class AllMustPermitStrategy extends AbilityStrategy {
3065
+ _decisive = null;
3032
3066
  evaluate() {
3033
- const matched = this.matchedPolicies();
3034
- if (matched.length === 0) {
3067
+ // 1. Нет совпавших политик → deny, но решающей политики нет
3068
+ if (!this.hasMatched()) {
3069
+ this._decisive = null;
3035
3070
  return AbilityPolicyEffect.deny;
3036
3071
  }
3037
- const allPermit = matched.every(p => p.effect === AbilityPolicyEffect.permit);
3038
- return allPermit ? AbilityPolicyEffect.permit : AbilityPolicyEffect.deny;
3072
+ // 2. Если есть deny она решающая
3073
+ const deny = this.firstDenied();
3074
+ if (deny) {
3075
+ this._decisive = deny;
3076
+ return AbilityPolicyEffect.deny;
3077
+ }
3078
+ this._decisive = this.firstPermitted();
3079
+ return AbilityPolicyEffect.permit;
3080
+ }
3081
+ decisivePolicy() {
3082
+ return this._decisive;
3039
3083
  }
3040
3084
  }
3041
3085
 
@@ -3057,8 +3101,20 @@ class AllMustPermitStrategy extends AbilityStrategy {
3057
3101
  * Result: permit (because at least one policy permitted)
3058
3102
  */
3059
3103
  class AnyPermitStrategy extends AbilityStrategy {
3104
+ _decisive = null;
3060
3105
  evaluate() {
3061
- 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;
3062
3118
  }
3063
3119
  }
3064
3120
 
@@ -3081,15 +3137,27 @@ class AnyPermitStrategy extends AbilityStrategy {
3081
3137
  * Result: deny (because deny overrides everything)
3082
3138
  */
3083
3139
  class DenyOverridesStrategy extends AbilityStrategy {
3140
+ _decisive = null;
3084
3141
  evaluate() {
3085
- if (this.hasDeny()) {
3142
+ // 1. Если есть deny — он решающий
3143
+ const deny = this.firstDenied();
3144
+ if (deny) {
3145
+ this._decisive = deny;
3086
3146
  return AbilityPolicyEffect.deny;
3087
3147
  }
3088
- if (this.hasPermit()) {
3148
+ // 2. Если есть permit — он решающий
3149
+ const permit = this.firstPermitted();
3150
+ if (permit) {
3151
+ this._decisive = permit;
3089
3152
  return AbilityPolicyEffect.permit;
3090
3153
  }
3154
+ // 3. Нет ни permit, ни deny → deny по умолчанию
3155
+ this._decisive = null;
3091
3156
  return AbilityPolicyEffect.deny;
3092
3157
  }
3158
+ decisivePolicy() {
3159
+ return this._decisive;
3160
+ }
3093
3161
  }
3094
3162
 
3095
3163
  /**
@@ -3110,9 +3178,20 @@ class DenyOverridesStrategy extends AbilityStrategy {
3110
3178
  * Result: permit (P2 is the first applicable)
3111
3179
  */
3112
3180
  class FirstMatchStrategy extends AbilityStrategy {
3181
+ _decisive = null;
3113
3182
  evaluate() {
3114
3183
  const first = this.firstMatched();
3115
- 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;
3116
3195
  }
3117
3196
  }
3118
3197
 
@@ -3133,13 +3212,21 @@ class FirstMatchStrategy extends AbilityStrategy {
3133
3212
  * Result: deny (more than one applicable policy)
3134
3213
  */
3135
3214
  class OnlyOneApplicableStrategy extends AbilityStrategy {
3215
+ _decisive = null;
3136
3216
  evaluate() {
3137
3217
  const matched = this.matchedPolicies();
3218
+ // 1. Ровно одна совпавшая политика → она решающая
3138
3219
  if (matched.length === 1) {
3220
+ this._decisive = matched[0];
3139
3221
  return matched[0].effect;
3140
3222
  }
3223
+ // 2. Иначе deny, решающей политики нет
3224
+ this._decisive = null;
3141
3225
  return AbilityPolicyEffect.deny;
3142
3226
  }
3227
+ decisivePolicy() {
3228
+ return this._decisive;
3229
+ }
3143
3230
  }
3144
3231
 
3145
3232
  /**
@@ -3161,15 +3248,27 @@ class OnlyOneApplicableStrategy extends AbilityStrategy {
3161
3248
  * Result: permit (permit overrides deny)
3162
3249
  */
3163
3250
  class PermitOverridesStrategy extends AbilityStrategy {
3251
+ _decisive = null;
3164
3252
  evaluate() {
3165
- if (this.hasPermit()) {
3253
+ // 1. Если есть permit — он выигрывает
3254
+ const permit = this.matchedPolicies().find(p => p.effect === AbilityPolicyEffect.permit);
3255
+ if (permit) {
3256
+ this._decisive = permit;
3166
3257
  return AbilityPolicyEffect.permit;
3167
3258
  }
3168
- 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;
3169
3263
  return AbilityPolicyEffect.deny;
3170
3264
  }
3265
+ // 3. Нет ни permit, ни deny → deny по умолчанию
3266
+ this._decisive = null;
3171
3267
  return AbilityPolicyEffect.deny;
3172
3268
  }
3269
+ decisivePolicy() {
3270
+ return this._decisive;
3271
+ }
3173
3272
  }
3174
3273
 
3175
3274
  /**
@@ -3189,9 +3288,20 @@ class PermitOverridesStrategy extends AbilityStrategy {
3189
3288
  * Result: permit (P3 is the last applicable)
3190
3289
  */
3191
3290
  class SequentialLastMatchStrategy extends AbilityStrategy {
3291
+ _decisive = null;
3192
3292
  evaluate() {
3193
3293
  const last = this.lastMatched();
3194
- 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;
3195
3305
  }
3196
3306
  }
3197
3307
 
@@ -3213,13 +3323,23 @@ class SequentialLastMatchStrategy extends AbilityStrategy {
3213
3323
  * Result: permit (P2 has higher priority)
3214
3324
  */
3215
3325
  class PriorityStrategy extends AbilityStrategy {
3326
+ _decisive = null;
3216
3327
  evaluate() {
3217
3328
  const matched = this.matchedPolicies();
3329
+ // 1. Нет совпавших политик → deny, решающей политики нет
3218
3330
  if (matched.length === 0) {
3331
+ this._decisive = null;
3219
3332
  return AbilityPolicyEffect.deny;
3220
3333
  }
3334
+ // 2. Сортируем по приоритету (больший приоритет — выше)
3221
3335
  const sorted = [...matched].sort((a, b) => b.priority - a.priority);
3222
- 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;
3223
3343
  }
3224
3344
  }
3225
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.2",
4
+ "version": "3.6.4",
5
5
  "description": "Via-Profit Ability service",
6
6
  "keywords": [
7
7
  "ability",