@via-profit/ability 3.6.0 → 3.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.d.ts +20 -13
  2. package/dist/index.js +443 -213
  3. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -134,6 +134,7 @@ declare class AbilityRule<Resources extends object = object, Environment extends
134
134
  resource: AbilityRuleConfig['resource'];
135
135
  condition: AbilityConditionType;
136
136
  }>): AbilityRule<Resources, Environment>;
137
+ hash(): string;
137
138
  static equals<Resources extends object = object, Environment extends object = object>(subject: string, resource: AbilityRuleConfig['resource']): AbilityRule<Resources, Environment>;
138
139
  static notEquals<Resources extends object = object, Environment extends object = object>(subject: string, resource: AbilityRuleConfig['resource']): AbilityRule<Resources, Environment>;
139
140
  static contains<Resources extends object = object, Environment extends object = object>(subject: string, resource: AbilityRuleConfig['resource']): AbilityRule<Resources, Environment>;
@@ -199,6 +200,7 @@ declare class AbilityRuleSet<R extends ResourceObject = Record<string, unknown>,
199
200
  compareMethod: AbilityCompareType;
200
201
  rules: AbilityRule<R, E>[];
201
202
  }>): AbilityRuleSet<R, E>;
203
+ hash(): string;
202
204
  static and(rules: AbilityRule[]): AbilityRuleSet<Record<string, unknown>, Record<string, unknown>>;
203
205
  static or(rules: AbilityRule[]): AbilityRuleSet<Record<string, unknown>, Record<string, unknown>>;
204
206
  }
@@ -276,15 +278,15 @@ declare class AbilityPolicy<R extends ResourceObject = Record<string, unknown>,
276
278
  * one of the rules returns as «permit»
277
279
  */
278
280
  compareMethod: AbilityCompareType;
281
+ /**
282
+ * Policy ID
283
+ */
284
+ id: string;
279
285
  /**
280
286
  * Policy name
281
287
  */
282
288
  name: string;
283
289
  description?: string | null;
284
- /**
285
- * Policy ID
286
- */
287
- id: string;
288
290
  /**
289
291
  * Running the `enforce` or `resolve` method
290
292
  * will select only those from all passed policies that fall under the specified permission key.
@@ -325,6 +327,7 @@ declare class AbilityPolicy<R extends ResourceObject = Record<string, unknown>,
325
327
  compareMethod: AbilityCompareType;
326
328
  ruleSet: AbilityRuleSet<R, E>[];
327
329
  }>): AbilityPolicy<R, E>;
330
+ hash(): string;
328
331
  }
329
332
 
330
333
  type Primitive = string | number | boolean | null | undefined;
@@ -336,6 +339,7 @@ type EnvironmentObject = Record<string, unknown>;
336
339
  type ResourcesMap = Record<string, ResourceObject>;
337
340
  declare class AbilityTypeGenerator {
338
341
  readonly policies: readonly AbilityPolicy[];
342
+ private readonly policyEntries;
339
343
  constructor(policies: readonly AbilityPolicy[]);
340
344
  /**
341
345
  * Generates TypeScript type definitions based on the provided policies.
@@ -411,8 +415,8 @@ declare class AbilityResult<R extends ResourceObject = Record<string, unknown>,
411
415
  * Useful for debugging, logging, or building UI tools that visualize permission logic.
412
416
  */
413
417
  explain(): readonly AbilityExplain[];
414
- isAllowed(): boolean;
415
- isDenied(): boolean;
418
+ isAllowed: () => boolean;
419
+ isDenied: () => boolean;
416
420
  }
417
421
 
418
422
  interface AbilityResolverOptions<TTags extends string> {
@@ -420,6 +424,7 @@ interface AbilityResolverOptions<TTags extends string> {
420
424
  }
421
425
  type ExtractResources<P> = P extends AbilityPolicy<infer R, any, any> ? R : never;
422
426
  type ExtractEnvironment<P> = P extends AbilityPolicy<any, infer E, any> ? E : never;
427
+ type ExtractPermission<R> = R extends AbilityResolver<infer P, any, any> ? keyof ExtractResources<P> & string : never;
423
428
  type ExtractResourceByPermission<P, Perm extends string> = P extends AbilityPolicy<infer R, any, any> ? (Perm extends keyof R ? R[Perm] : never) : never;
424
429
  type ExtractEnvironmentByPermission<P, Perm extends string> = P extends AbilityPolicy<any, infer E, any> ? (Perm extends keyof E ? E[Perm] : never) : never;
425
430
  declare class AbilityResolver<P extends AbilityPolicy<any, any, any>, S extends AbilityStrategy<P extends AbilityPolicy<infer R, infer E, any> ? R : never, P extends AbilityPolicy<any, infer E, any> ? E : never>, TTags extends string = P extends AbilityPolicy<any, any, infer T> ? T : never> {
@@ -440,14 +445,16 @@ declare class AbilityResolver<P extends AbilityPolicy<any, any, any>, S extends
440
445
  resolve<Permission extends keyof ExtractResources<P> & string>(permission: Permission, resource: ExtractResourceByPermission<P, Permission>, environment?: ExtractEnvironmentByPermission<P, Permission>): AbilityResult<ExtractResourceByPermission<P, Permission>, ExtractEnvironment<P>>;
441
446
  enforce<Permission extends keyof ExtractResources<P> & string>(permission: Permission, resource: ExtractResourceByPermission<P, Permission>, environment?: ExtractEnvironmentByPermission<P, Permission>): void | never;
442
447
  /**
448
+ * @deprecated - will be removed
449
+ *
443
450
  * Check if the permission key is contained in another permission key
444
451
  * @param permissionA - The first permission to check
445
452
  * @param permissionB - The second permission to check
446
453
  */
447
454
  static isInPermissionContain(permissionA: string, permissionB: string): boolean;
448
455
  private toArray;
449
- private normalizePermission;
450
- private static matchPermissions;
456
+ static normalizePermission(permission: string): string;
457
+ static matchPermissions(policySegments: string[], inputSegments: string[]): boolean;
451
458
  }
452
459
 
453
460
  declare class AbilityJSONParser {
@@ -456,13 +463,13 @@ declare class AbilityJSONParser {
456
463
  * @param configs - Array of policy configurations
457
464
  * @returns Array of AbilityPolicy instances
458
465
  */
459
- static parse<Resource extends ResourceObject>(configs: readonly AbilityPolicyConfig[]): AbilityPolicy<Resource>[];
460
- static parsePolicy<Resource extends ResourceObject = Record<string, unknown>>(config: AbilityPolicyConfig): AbilityPolicy<Resource>;
461
- static parseRule<Resources extends object>(config: AbilityRuleConfig): AbilityRule<Resources>;
466
+ static parse<R extends ResourceObject, E extends EnvironmentObject, T extends string = string>(configs: readonly AbilityPolicyConfig[]): AbilityPolicy<R, E, T>[];
467
+ static parsePolicy<R extends ResourceObject, E extends EnvironmentObject, T extends string = string>(config: AbilityPolicyConfig): AbilityPolicy<R, E, T>;
468
+ static parseRule<R extends ResourceObject, E extends EnvironmentObject>(config: AbilityRuleConfig): AbilityRule<R, E>;
462
469
  /**
463
470
  * Parse the config JSON format to Group class instance
464
471
  */
465
- static parseRuleSet<Resource extends ResourceObject = Record<string, unknown>>(config: AbilityRuleSetConfig): AbilityRuleSet<Resource>;
472
+ static parseRuleSet<R extends ResourceObject, E extends EnvironmentObject>(config: AbilityRuleSetConfig): AbilityRuleSet<R, E>;
466
473
  static ruleToJSON(rule: AbilityRule): AbilityRuleConfig;
467
474
  static ruleSetToJSON(ruleSet: AbilityRuleSet): AbilityRuleSetConfig;
468
475
  static policyToJSON(policy: AbilityPolicy): AbilityPolicyConfig;
@@ -800,4 +807,4 @@ declare class PriorityStrategy<R extends ResourceObject, E extends EnvironmentOb
800
807
  }
801
808
 
802
809
  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 };
803
- export type { AbilityCompareType, AbilityConditionCode, AbilityConditionLiteral, AbilityConditionType, AbilityExplainConfig, AbilityExplainType, AbilityMatchType, AbilityPolicyConfig, AbilityPolicyConstructorProps, AbilityPolicyEffectType, AbilityResolverOptions, AbilityRuleConfig, AbilityRuleConstructorProps, AbilityRuleSetConfig, AbilityRuleSetConstructorProps, EnvironmentObject, NestedDict, Primitive, ResourceObject, ResourcesMap, TokenType, TokenTypeCode };
810
+ export type { AbilityCompareType, AbilityConditionCode, AbilityConditionLiteral, AbilityConditionType, AbilityExplainConfig, AbilityExplainType, AbilityMatchType, AbilityPolicyConfig, AbilityPolicyConstructorProps, AbilityPolicyEffectType, AbilityResolverOptions, AbilityRuleConfig, AbilityRuleConstructorProps, AbilityRuleSetConfig, AbilityRuleSetConstructorProps, EnvironmentObject, ExtractEnvironment, ExtractEnvironmentByPermission, ExtractPermission, ExtractResourceByPermission, ExtractResources, NestedDict, Primitive, ResourceObject, ResourcesMap, TokenType, TokenTypeCode };
package/dist/index.js CHANGED
@@ -126,10 +126,198 @@ const AbilityMatch = {
126
126
  disabled: brand$2('disabled'),
127
127
  };
128
128
 
129
+ class AbilityExplain {
130
+ type;
131
+ children;
132
+ name;
133
+ match;
134
+ constructor(config, children = []) {
135
+ this.type = config.type;
136
+ this.children = children;
137
+ this.name = config.name;
138
+ 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';
146
+ }
147
+ out += `${pad}${mark} ${this.type} «${this.name}» is ${this.match}`;
148
+ this.children.forEach(child => {
149
+ out += '\n' + child.toString(indent + 1);
150
+ });
151
+ return out;
152
+ }
153
+ }
154
+ class AbilityExplainRule extends AbilityExplain {
155
+ constructor(rule) {
156
+ super({
157
+ type: 'rule',
158
+ match: rule.state,
159
+ name: rule.name,
160
+ });
161
+ }
162
+ }
163
+ class AbilityExplainRuleSet extends AbilityExplain {
164
+ constructor(ruleSet) {
165
+ const children = ruleSet.rules.map(rule => new AbilityExplainRule(rule));
166
+ super({
167
+ type: 'ruleSet',
168
+ match: ruleSet.state,
169
+ name: ruleSet.name,
170
+ }, children);
171
+ }
172
+ }
173
+ class AbilityExplainPolicy extends AbilityExplain {
174
+ constructor(policy) {
175
+ const children = policy.ruleSet.map(ruleSet => new AbilityExplainRuleSet(ruleSet));
176
+ super({
177
+ type: 'policy',
178
+ name: policy.priority > -1 ? `@priority ${policy.priority} ${policy.name}` : policy.name,
179
+ match: policy.matchState,
180
+ }, children);
181
+ }
182
+ }
183
+
184
+ class AbilityResult {
185
+ effect;
186
+ strategy;
187
+ constructor(effect, strategy) {
188
+ this.effect = effect;
189
+ this.strategy = strategy;
190
+ }
191
+ /**
192
+ * Returns a list of explanations for each policy involved in the ability evaluation.
193
+ * Each item describes how a specific policy contributed to the final permission result.
194
+ *
195
+ * Useful for debugging, logging, or building UI tools that visualize permission logic.
196
+ */
197
+ explain() {
198
+ return this.strategy.policies.map(policy => {
199
+ return new AbilityExplainPolicy(policy);
200
+ });
201
+ }
202
+ isAllowed = () => {
203
+ return this.strategy.isAllowed();
204
+ };
205
+ isDenied = () => {
206
+ return this.strategy.isDenied();
207
+ };
208
+ }
209
+
210
+ class AbilityResolver {
211
+ StrategyClass;
212
+ policyEntries;
213
+ constructor(
214
+ /**
215
+ * `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.
216
+ */
217
+ policyOrListOfPolicies, strategy, options = {}) {
218
+ const policies = this.toArray(policyOrListOfPolicies);
219
+ const filtered = options.tags
220
+ ? policies.filter(p => p.tags.some(tag => options.tags.includes(tag)))
221
+ : policies;
222
+ const sorted = [...filtered].sort((a, b) => b.priority - a.priority);
223
+ this.policyEntries = sorted.map(policy => ({
224
+ policy,
225
+ normalizedPermission: AbilityResolver.normalizePermission(policy.permission),
226
+ segments: AbilityResolver.normalizePermission(policy.permission).split('.'),
227
+ }));
228
+ this.StrategyClass = strategy;
229
+ }
230
+ /**
231
+ * Resolve policy for the resource and permission key
232
+ *
233
+ * @param permission - Permission key
234
+ * @param resource - Resource
235
+ * @param environment
236
+ */
237
+ resolve(permission, resource, environment) {
238
+ const inputNormalized = AbilityResolver.normalizePermission(String(permission));
239
+ const inputSegments = inputNormalized.split('.');
240
+ const filteredPolicies = this.policyEntries
241
+ .filter(entry => AbilityResolver.matchPermissions(entry.segments, inputSegments))
242
+ .map(entry => entry.policy);
243
+ // 2. check the policies
244
+ for (const policy of filteredPolicies) {
245
+ if (policy.disabled) {
246
+ continue;
247
+ }
248
+ const policyMatchState = policy.check(resource, environment);
249
+ if (policyMatchState === AbilityMatch.pending) {
250
+ throw new AbilityError(`The policy "${policy.name}" is still in a pending state. Make sure to call "check" to evaluate the policy before resolving permissions.`);
251
+ }
252
+ }
253
+ // 3. Use strategy
254
+ const strategy = new this.StrategyClass(filteredPolicies);
255
+ const effect = strategy.evaluate();
256
+ return new AbilityResult(effect, strategy);
257
+ }
258
+ enforce(permission, resource, environment) {
259
+ const result = this.resolve(permission, resource, environment);
260
+ if (result.isDenied()) {
261
+ throw new AbilityError(`Permission denied`);
262
+ }
263
+ }
264
+ /**
265
+ * @deprecated - will be removed
266
+ *
267
+ * Check if the permission key is contained in another permission key
268
+ * @param permissionA - The first permission to check
269
+ * @param permissionB - The second permission to check
270
+ */
271
+ static isInPermissionContain(permissionA, permissionB) {
272
+ const A = permissionA.split('.');
273
+ const B = permissionB.split('.');
274
+ const [longer, shorter] = A.length >= B.length ? [A, B] : [B, A];
275
+ return shorter.every((chunk, i) => {
276
+ return chunk === '*' || longer[i] === '*' || chunk === longer[i];
277
+ });
278
+ }
279
+ toArray(value) {
280
+ return [...(Array.isArray(value) ? value : [value])];
281
+ }
282
+ static normalizePermission(permission) {
283
+ return permission
284
+ .trim()
285
+ .replace(/^permission\./, '') // remove prefix
286
+ .replace(/\.+/g, '.') // collapse multiple dots
287
+ .toLowerCase(); // optional: make case-insensitive
288
+ }
289
+ static matchPermissions(policySegments, inputSegments) {
290
+ const maxLen = Math.max(policySegments.length, inputSegments.length);
291
+ for (let i = 0; i < maxLen; i++) {
292
+ const pSeg = policySegments[i];
293
+ const iSeg = inputSegments[i];
294
+ if (pSeg === undefined) {
295
+ return false;
296
+ }
297
+ if (pSeg === '*') {
298
+ continue; // '*'
299
+ }
300
+ if (iSeg === undefined) {
301
+ return false;
302
+ }
303
+ if (pSeg !== iSeg) {
304
+ return false;
305
+ }
306
+ }
307
+ return true;
308
+ }
309
+ }
310
+
129
311
  class AbilityTypeGenerator {
130
312
  policies;
313
+ policyEntries;
131
314
  constructor(policies) {
132
315
  this.policies = policies;
316
+ this.policyEntries = policies.map(policy => ({
317
+ policy,
318
+ normalizedPermission: AbilityResolver.normalizePermission(policy.permission),
319
+ segments: AbilityResolver.normalizePermission(policy.permission).split('.'),
320
+ }));
133
321
  }
134
322
  /**
135
323
  * Generates TypeScript type definitions based on the provided policies.
@@ -277,21 +465,6 @@ class AbilityTypeGenerator {
277
465
  * @returns TypeScript array type as string
278
466
  */
279
467
  getArrayType(resource) {
280
- // if (Array.isArray(resource)) {
281
- // if (resource.length === 0) {
282
- // return 'readonly unknown[]';
283
- // }
284
- // // Determine types of array elements
285
- // const elementTypes = new Set(resource.map(item => this.getPrimitiveType(item)));
286
- // const elementType =
287
- // elementTypes.size === 1
288
- // ? Array.from(elementTypes)[0]
289
- // : `(${Array.from(elementTypes).join(' | ')})`;
290
- // return `readonly ${elementType}[]`;
291
- // }
292
- // // If resource is not an array but condition is in/not_in,
293
- // // it expects an array of such elements
294
- // return `readonly ${this.getPrimitiveType(resource)}[]`;
295
468
  const elementType = this.getInArrayType(resource);
296
469
  return `readonly ${elementType}[]`;
297
470
  }
@@ -387,18 +560,43 @@ class AbilityTypeGenerator {
387
560
  output += '// Do not edit manually\n';
388
561
  output += 'export type Resources = {\n';
389
562
  const sortedActions = Object.keys(structure).sort();
390
- sortedActions.forEach(action => {
391
- const actionObj = structure[action];
563
+ sortedActions.forEach(permission => {
564
+ const actionObj = structure[permission];
392
565
  const isEmpty = Object.keys(actionObj).length === 0;
566
+ const inputNormalized = AbilityResolver.normalizePermission(permission);
567
+ const inputSegments = inputNormalized.split('.');
568
+ const filteredPolicies = this.policyEntries
569
+ .filter(entry => AbilityResolver.matchPermissions(entry.segments, inputSegments))
570
+ .map(entry => entry.policy);
571
+ // Effects
572
+ const effects = [...new Set(filteredPolicies.map(p => p.effect))].sort();
573
+ // Policies list
574
+ const items = filteredPolicies
575
+ .sort((a, b) => a.id.localeCompare(b.id))
576
+ .map(p => {
577
+ const effect = p.effect.padEnd(6, ' '); // permit / deny / audit
578
+ const displayName = p.name === p.id ? 'Unnamed policy' : p.name;
579
+ return ` * - ${effect} ${p.id} "${displayName}"`;
580
+ })
581
+ .join('\n');
582
+ //
583
+ output += `
584
+ /**
585
+ * Permission: ${permission}
586
+ * Effects: ${effects.join(', ')}
587
+ * Policies:
588
+ ${items}
589
+ */
590
+ `;
393
591
  if (isEmpty) {
394
- // Пустой объект → undefined
395
- output += ` ['${action}']: undefined;\n`;
592
+ // empty object → undefined
593
+ output += ` ['${permission}']: undefined;\n`;
396
594
  }
397
595
  else {
398
- // Непустой объект → как раньше
399
- output += ` ['${action}']: {\n`;
596
+ // not empty object
597
+ output += ` ['${permission}']: {\n`;
400
598
  output += this.formatNestedObject(actionObj, 4);
401
- output += ' };\n';
599
+ output += ' } | null | undefined;\n';
402
600
  }
403
601
  });
404
602
  output += '}\n';
@@ -412,15 +610,37 @@ class AbilityTypeGenerator {
412
610
  output += `\n\nexport type PolicyTags = ${tagsUnion};\n`;
413
611
  // environments
414
612
  output += '\n\nexport type Environment = {\n';
415
- Object.entries(environment).forEach(([action, envObj]) => {
613
+ Object.entries(environment).forEach(([permission, envObj]) => {
416
614
  const isEmpty = Object.keys(envObj).length === 0;
615
+ const inputNormalized = AbilityResolver.normalizePermission(permission);
616
+ const inputSegments = inputNormalized.split('.');
617
+ const filteredPolicies = this.policyEntries
618
+ .filter(entry => AbilityResolver.matchPermissions(entry.segments, inputSegments))
619
+ .map(entry => entry.policy);
620
+ const effects = [...new Set(filteredPolicies.map(p => p.effect))].sort();
621
+ const items = filteredPolicies
622
+ .sort((a, b) => a.id.localeCompare(b.id))
623
+ .map(p => {
624
+ const effect = p.effect.padEnd(6, ' ');
625
+ const displayName = p.name === p.id ? 'Unnamed policy' : p.name;
626
+ return ` * - ${effect} ${p.id} "${displayName}"`;
627
+ })
628
+ .join('\n');
629
+ output += `
630
+ /**
631
+ * Permission: ${permission}
632
+ * Effects: ${effects.join(', ')}
633
+ * Policies:
634
+ ${items}
635
+ */
636
+ `;
417
637
  if (isEmpty) {
418
- output += ` ['${action}']: undefined;\n`;
638
+ output += ` ['${permission}']: undefined;\n`;
419
639
  }
420
640
  else {
421
- output += ` ['${action}']: {\n`;
641
+ output += ` ['${permission}']: {\n`;
422
642
  output += this.formatNestedObject(envObj, 4);
423
- output += ' };\n';
643
+ output += ' } | null | undefined;\n';
424
644
  }
425
645
  });
426
646
  output += '}\n';
@@ -444,69 +664,126 @@ class AbilityTypeGenerator {
444
664
  // Nested object
445
665
  output += `${spaces}readonly ${key}: {\n`;
446
666
  output += this.formatNestedObject(value, indent + 2);
447
- output += `${spaces}};\n`;
667
+ output += `${spaces}} | null | undefined;\n`;
448
668
  }
449
669
  else {
450
670
  // Primitive type
451
- output += `${spaces}readonly ${key}: ${value};\n`;
671
+ const va = [String(value)];
672
+ let v = String(value);
673
+ if (!v.match(/unknown/)) {
674
+ if (!v.match(/null/)) {
675
+ va.push('null');
676
+ }
677
+ if (!v.match(/undefined/)) {
678
+ va.push('undefined');
679
+ }
680
+ }
681
+ output += `${spaces}readonly ${key}: ${va.join(' | ')} \n`;
452
682
  }
453
683
  });
454
684
  return output;
455
685
  }
456
686
  }
457
687
 
458
- class AbilityExplain {
459
- type;
460
- children;
461
- name;
462
- match;
463
- constructor(config, children = []) {
464
- this.type = config.type;
465
- this.children = children;
466
- this.name = config.name;
467
- this.match = config.match;
468
- }
469
- toString(indent = 0) {
470
- const pad = ' '.repeat(indent);
471
- const mark = this.match === AbilityMatch.match ? '✓' : '✗';
472
- let out = '';
473
- if (this.type === 'policy') {
474
- out += '\n';
688
+ class AbilityHash {
689
+ static sha1(message) {
690
+ const msgBytes = AbilityHash.stringToBytes(message);
691
+ const msgBitLength = msgBytes.length * 8;
692
+ const withOne = new Uint8Array(msgBytes.length + 1);
693
+ withOne.set(msgBytes, 0);
694
+ withOne[msgBytes.length] = 0x80;
695
+ let zeroBytes = (56 - (withOne.length % 64) + 64) % 64;
696
+ const padded = new Uint8Array(withOne.length + zeroBytes + 8);
697
+ padded.set(withOne, 0);
698
+ const bitLenHigh = Math.floor(msgBitLength / 0x100000000);
699
+ const bitLenLow = msgBitLength >>> 0;
700
+ padded[padded.length - 8] = (bitLenHigh >>> 24) & 0xff;
701
+ padded[padded.length - 7] = (bitLenHigh >>> 16) & 0xff;
702
+ padded[padded.length - 6] = (bitLenHigh >>> 8) & 0xff;
703
+ padded[padded.length - 5] = bitLenHigh & 0xff;
704
+ padded[padded.length - 4] = (bitLenLow >>> 24) & 0xff;
705
+ padded[padded.length - 3] = (bitLenLow >>> 16) & 0xff;
706
+ padded[padded.length - 2] = (bitLenLow >>> 8) & 0xff;
707
+ padded[padded.length - 1] = bitLenLow & 0xff;
708
+ let h0 = 0x67452301;
709
+ let h1 = 0xefcdab89;
710
+ let h2 = 0x98badcfe;
711
+ let h3 = 0x10325476;
712
+ let h4 = 0xc3d2e1f0;
713
+ const w = new Array(80);
714
+ for (let i = 0; i < padded.length; i += 64) {
715
+ for (let j = 0; j < 16; j++) {
716
+ const idx = i + j * 4;
717
+ w[j] =
718
+ (padded[idx] << 24) | (padded[idx + 1] << 16) | (padded[idx + 2] << 8) | padded[idx + 3];
719
+ }
720
+ for (let j = 16; j < 80; j++) {
721
+ w[j] = AbilityHash.leftRotate(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1);
722
+ }
723
+ let a = h0;
724
+ let b = h1;
725
+ let c = h2;
726
+ let d = h3;
727
+ let e = h4;
728
+ for (let j = 0; j < 80; j++) {
729
+ let f;
730
+ let k;
731
+ if (j < 20) {
732
+ f = (b & c) | (~b & d);
733
+ k = 0x5a827999;
734
+ }
735
+ else {
736
+ if (j < 40) {
737
+ f = b ^ c ^ d;
738
+ k = 0x6ed9eba1;
739
+ }
740
+ else {
741
+ if (j < 60) {
742
+ f = (b & c) | (b & d) | (c & d);
743
+ k = 0x8f1bbcdc;
744
+ }
745
+ else {
746
+ f = b ^ c ^ d;
747
+ k = 0xca62c1d6;
748
+ }
749
+ }
750
+ }
751
+ const temp = (AbilityHash.leftRotate(a, 5) + f + e + k + (w[j] | 0)) | 0;
752
+ e = d;
753
+ d = c;
754
+ c = AbilityHash.leftRotate(b, 30);
755
+ b = a;
756
+ a = temp;
757
+ }
758
+ h0 = (h0 + a) | 0;
759
+ h1 = (h1 + b) | 0;
760
+ h2 = (h2 + c) | 0;
761
+ h3 = (h3 + d) | 0;
762
+ h4 = (h4 + e) | 0;
763
+ }
764
+ return [
765
+ AbilityHash.toHex32(h0),
766
+ AbilityHash.toHex32(h1),
767
+ AbilityHash.toHex32(h2),
768
+ AbilityHash.toHex32(h3),
769
+ AbilityHash.toHex32(h4),
770
+ ].join('');
771
+ }
772
+ static leftRotate(value, bits) {
773
+ return ((value << bits) | (value >>> (32 - bits))) >>> 0;
774
+ }
775
+ static toHex32(num) {
776
+ return (num >>> 0).toString(16).padStart(8, '0');
777
+ }
778
+ static stringToBytes(str) {
779
+ if (typeof TextEncoder !== 'undefined') {
780
+ const encoder = new TextEncoder();
781
+ return encoder.encode(str);
782
+ }
783
+ else {
784
+ const buf = Buffer.from(str, 'utf8');
785
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
475
786
  }
476
- out += `${pad}${mark} ${this.type} «${this.name}» is ${this.match}`;
477
- this.children.forEach(child => {
478
- out += '\n' + child.toString(indent + 1);
479
- });
480
- return out;
481
- }
482
- }
483
- class AbilityExplainRule extends AbilityExplain {
484
- constructor(rule) {
485
- super({
486
- type: 'rule',
487
- match: rule.state,
488
- name: rule.name,
489
- });
490
- }
491
- }
492
- class AbilityExplainRuleSet extends AbilityExplain {
493
- constructor(ruleSet) {
494
- const children = ruleSet.rules.map(rule => new AbilityExplainRule(rule));
495
- super({
496
- type: 'ruleSet',
497
- match: ruleSet.state,
498
- name: ruleSet.name,
499
- }, children);
500
- }
501
- }
502
- class AbilityExplainPolicy extends AbilityExplain {
503
- constructor(policy) {
504
- const children = policy.ruleSet.map(ruleSet => new AbilityExplainRuleSet(ruleSet));
505
- super({
506
- type: 'policy',
507
- name: policy.priority > -1 ? `@priority ${policy.priority} ${policy.name}` : policy.name,
508
- match: policy.matchState,
509
- }, children);
510
787
  }
511
788
  }
512
789
 
@@ -527,15 +804,15 @@ class AbilityPolicy {
527
804
  * one of the rules returns as «permit»
528
805
  */
529
806
  compareMethod = AbilityCompare.and;
807
+ /**
808
+ * Policy ID
809
+ */
810
+ id;
530
811
  /**
531
812
  * Policy name
532
813
  */
533
814
  name;
534
815
  description;
535
- /**
536
- * Policy ID
537
- */
538
- id;
539
816
  /**
540
817
  * Running the `enforce` or `resolve` method
541
818
  * will select only those from all passed policies that fall under the specified permission key.
@@ -546,8 +823,6 @@ class AbilityPolicy {
546
823
  tags;
547
824
  constructor(params) {
548
825
  const { name, description, id, permission, effect, compareMethod = AbilityCompare.and, priority, disabled, tags, } = params;
549
- this.id = id || `policy:${effect}:${permission}`;
550
- this.name = name || this.id;
551
826
  this.permission = permission;
552
827
  this.description = description;
553
828
  this.effect = effect;
@@ -555,6 +830,8 @@ class AbilityPolicy {
555
830
  this.priority = typeof priority === 'number' ? priority : -1;
556
831
  this.disabled = typeof disabled === 'boolean' ? disabled : false;
557
832
  this.tags = (tags || []);
833
+ this.id = id || `p_${this.hash().slice(0, 10)}`;
834
+ this.name = name || this.id;
558
835
  }
559
836
  /**
560
837
  * Add rule set to the policy
@@ -668,6 +945,24 @@ class AbilityPolicy {
668
945
  }
669
946
  return policy;
670
947
  }
948
+ hash() {
949
+ const parts = [
950
+ `permission:${this.permission}`,
951
+ `effect:${this.effect}`,
952
+ `compareMethod:${this.compareMethod}`,
953
+ `priority:${this.priority}`,
954
+ `disabled:${this.disabled}`,
955
+ ];
956
+ if (this.tags && this.tags.length > 0) {
957
+ parts.push(`tags:${[...this.tags].sort().join(',')}`);
958
+ }
959
+ if (this.ruleSet && this.ruleSet.length > 0) {
960
+ const ruleHashes = this.ruleSet.map(r => r.hash());
961
+ parts.push(`rules:${ruleHashes.sort().join('|')}`);
962
+ }
963
+ const str = parts.join(';');
964
+ return AbilityHash.sha1(str);
965
+ }
671
966
  }
672
967
 
673
968
  function brand$1(code) {
@@ -678,127 +973,6 @@ const AbilityPolicyEffect = {
678
973
  permit: brand$1('permit'),
679
974
  };
680
975
 
681
- class AbilityResult {
682
- effect;
683
- strategy;
684
- constructor(effect, strategy) {
685
- this.effect = effect;
686
- this.strategy = strategy;
687
- }
688
- /**
689
- * Returns a list of explanations for each policy involved in the ability evaluation.
690
- * Each item describes how a specific policy contributed to the final permission result.
691
- *
692
- * Useful for debugging, logging, or building UI tools that visualize permission logic.
693
- */
694
- explain() {
695
- return this.strategy.policies.map(policy => {
696
- return new AbilityExplainPolicy(policy);
697
- });
698
- }
699
- isAllowed() {
700
- return this.strategy.isAllowed();
701
- }
702
- isDenied() {
703
- return this.strategy.isDenied();
704
- }
705
- }
706
-
707
- class AbilityResolver {
708
- StrategyClass;
709
- policyEntries;
710
- constructor(
711
- /**
712
- * `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.
713
- */
714
- policyOrListOfPolicies, strategy, options = {}) {
715
- const policies = this.toArray(policyOrListOfPolicies);
716
- const filtered = options.tags
717
- ? policies.filter(p => p.tags.some(tag => options.tags.includes(tag)))
718
- : policies;
719
- const sorted = [...filtered].sort((a, b) => b.priority - a.priority);
720
- this.policyEntries = sorted.map(policy => ({
721
- policy,
722
- normalizedPermission: this.normalizePermission(policy.permission),
723
- segments: this.normalizePermission(policy.permission).split('.'),
724
- }));
725
- this.StrategyClass = strategy;
726
- }
727
- /**
728
- * Resolve policy for the resource and permission key
729
- *
730
- * @param permission - Permission key
731
- * @param resource - Resource
732
- * @param environment
733
- */
734
- resolve(permission, resource, environment) {
735
- const inputNormalized = this.normalizePermission(String(permission));
736
- const inputSegments = inputNormalized.split('.');
737
- const filteredPolicies = this.policyEntries
738
- .filter(entry => AbilityResolver.matchPermissions(entry.segments, inputSegments))
739
- .map(entry => entry.policy);
740
- // 2. check the policies
741
- for (const policy of filteredPolicies) {
742
- if (policy.disabled) {
743
- continue;
744
- }
745
- const policyMatchState = policy.check(resource, environment);
746
- if (policyMatchState === AbilityMatch.pending) {
747
- throw new AbilityError(`The policy "${policy.name}" is still in a pending state. Make sure to call "check" to evaluate the policy before resolving permissions.`);
748
- }
749
- }
750
- // 3. Use strategy
751
- const strategy = new this.StrategyClass(filteredPolicies);
752
- const effect = strategy.evaluate();
753
- return new AbilityResult(effect, strategy);
754
- }
755
- enforce(permission, resource, environment) {
756
- const result = this.resolve(permission, resource, environment);
757
- if (result.isDenied()) {
758
- throw new AbilityError(`Permission denied`);
759
- }
760
- }
761
- /**
762
- * Check if the permission key is contained in another permission key
763
- * @param permissionA - The first permission to check
764
- * @param permissionB - The second permission to check
765
- */
766
- static isInPermissionContain(permissionA, permissionB) {
767
- const A = permissionA.split('.');
768
- const B = permissionB.split('.');
769
- const [longer, shorter] = A.length >= B.length ? [A, B] : [B, A];
770
- return shorter.every((chunk, i) => {
771
- return chunk === '*' || longer[i] === '*' || chunk === longer[i];
772
- });
773
- }
774
- toArray(value) {
775
- return [...(Array.isArray(value) ? value : [value])];
776
- }
777
- normalizePermission(permission) {
778
- return permission
779
- .trim()
780
- .replace(/^permission\./, '') // remove prefix
781
- .replace(/\.+/g, '.') // collapse multiple dots
782
- .toLowerCase(); // optional: make case-insensitive
783
- }
784
- static matchPermissions(policySegments, inputSegments) {
785
- const maxLen = Math.max(policySegments.length, inputSegments.length);
786
- for (let i = 0; i < maxLen; i++) {
787
- const pSeg = policySegments[i];
788
- const iSeg = inputSegments[i];
789
- if (pSeg === undefined)
790
- return false; // policy короче – не матчит
791
- if (pSeg === '*')
792
- continue; // '*' матчит любой сегмент
793
- if (iSeg === undefined)
794
- return false; // входной permission короче
795
- if (pSeg !== iSeg)
796
- return false;
797
- }
798
- return true;
799
- }
800
- }
801
-
802
976
  /**
803
977
  * Represents a rule that defines a condition to be checked against a subject and resource.
804
978
  */
@@ -829,14 +1003,14 @@ class AbilityRule {
829
1003
  */
830
1004
  constructor(params) {
831
1005
  const { id, name, subject, resource, condition, disabled, description } = params;
832
- this.name = name || `rule:${JSON.stringify(subject)}:${condition}:${JSON.stringify(resource)}`;
833
- this.id = id || this.name;
834
1006
  this.description = description;
835
1007
  this.disabled = typeof disabled === 'boolean' ? disabled : false;
836
1008
  this.subject = subject;
837
1009
  this.resource = resource;
838
1010
  this.condition = condition;
839
1011
  this.state = this.disabled ? AbilityMatch.disabled : this.state;
1012
+ this.id = id || `r_${this.hash().slice(0, 10)}`;
1013
+ this.name = name || this.id;
840
1014
  }
841
1015
  static isPrimitive(v) {
842
1016
  return typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v === null;
@@ -1063,6 +1237,14 @@ class AbilityRule {
1063
1237
  condition: props.condition ?? this.condition,
1064
1238
  });
1065
1239
  }
1240
+ hash() {
1241
+ const parts = [];
1242
+ parts.push(`subject:${this.subject}`);
1243
+ parts.push(`resource:${JSON.stringify(this.resource)}`);
1244
+ parts.push(`condition:${this.condition}`);
1245
+ parts.push(`disabled:${this.disabled}`);
1246
+ return AbilityHash.sha1(parts.join(';'));
1247
+ }
1066
1248
  static equals(subject, resource) {
1067
1249
  return new AbilityRule({
1068
1250
  condition: AbilityCondition.equals,
@@ -1168,13 +1350,13 @@ class AbilityRuleSet {
1168
1350
  disabled;
1169
1351
  constructor(params) {
1170
1352
  const { name, id, compareMethod, isExcept, disabled, description } = params;
1171
- this.name = name || `ruleset:${compareMethod}`;
1172
- this.id = id || this.name;
1173
1353
  this.description = description;
1174
1354
  this.compareMethod = compareMethod;
1175
1355
  this.isExcept = isExcept;
1176
1356
  this.disabled = typeof disabled === 'boolean' ? disabled : false;
1177
1357
  this.state = this.disabled ? AbilityMatch.disabled : this.state;
1358
+ this.id = id || `g_${this.hash().slice(0, 10)}`;
1359
+ this.name = name || this.id;
1178
1360
  }
1179
1361
  addRule(rule) {
1180
1362
  this.rules.push(rule);
@@ -1236,6 +1418,16 @@ class AbilityRuleSet {
1236
1418
  }
1237
1419
  return next;
1238
1420
  }
1421
+ hash() {
1422
+ const ruleHashes = this.rules.map(r => r.hash()).sort();
1423
+ const parts = [
1424
+ `compareMethod:${this.compareMethod}`,
1425
+ `isExcept:${this.isExcept}`,
1426
+ `disabled:${this.disabled}`,
1427
+ `rules:${ruleHashes.join('|')}`,
1428
+ ];
1429
+ return AbilityHash.sha1(parts.join(';'));
1430
+ }
1239
1431
  static and(rules) {
1240
1432
  return new AbilityRuleSet({
1241
1433
  compareMethod: AbilityCompare.and,
@@ -1508,11 +1700,49 @@ class AbilityDSLLexer {
1508
1700
  readAnnotation() {
1509
1701
  const startLine = this.line;
1510
1702
  const startColumn = this.column;
1511
- let value = '';
1703
+ let raw = '';
1704
+ // читаем всю строку после @
1512
1705
  while (!this.isAtEnd() && !this.isNewline()) {
1513
- value += this.advance();
1706
+ raw += this.advance();
1707
+ }
1708
+ raw = raw.trim();
1709
+ // parse literals
1710
+ let result = '';
1711
+ let i = 0;
1712
+ let inString = false;
1713
+ let quote = null;
1714
+ let escaped = false;
1715
+ while (i < raw.length) {
1716
+ const ch = raw[i];
1717
+ if (inString) {
1718
+ if (escaped) {
1719
+ result += ch;
1720
+ escaped = false;
1721
+ }
1722
+ else if (ch === '\\') {
1723
+ escaped = true;
1724
+ }
1725
+ else if (ch === quote) {
1726
+ inString = false;
1727
+ quote = null;
1728
+ }
1729
+ else {
1730
+ result += ch;
1731
+ }
1732
+ i++;
1733
+ continue;
1734
+ }
1735
+ // start of string
1736
+ if (ch === '"' || ch === "'") {
1737
+ inString = true;
1738
+ quote = ch;
1739
+ i++;
1740
+ continue;
1741
+ }
1742
+ result += ch;
1743
+ i++;
1514
1744
  }
1515
- return new AbilityDSLToken(TokenTypes.ANNOTATION, value.trim(), startLine, startColumn);
1745
+ return new AbilityDSLToken(TokenTypes.ANNOTATION, result.trim(), startLine, startColumn);
1516
1746
  }
1517
1747
  readString() {
1518
1748
  const startLine = this.line;
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.0",
4
+ "version": "3.6.1",
5
5
  "description": "Via-Profit Ability service",
6
6
  "keywords": [
7
7
  "ability",