@via-profit/ability 3.2.0 → 3.3.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/SECURITY.md CHANGED
@@ -1,33 +1,33 @@
1
- # Security Policy
2
-
3
- ## Reporting a Vulnerability
4
-
5
- I take the security of `@via-profit/ability` seriously. If you discover a security vulnerability, please report it responsibly.
6
-
7
- ### How to Report a Vulnerability
8
-
9
- **Please DO NOT create a public GitHub issue for security vulnerabilities.**
10
-
11
- Instead, send the details directly to me:
12
-
13
- - **Email**: [delhsmail@gmail.com](mailto:delhsmail@gmail.com)
14
- - **Author**: Vasily Novosad
15
- - **Timezone**: UTC+5 (for coordinating response time)
16
-
17
- ### What to Include
18
-
19
- To help me address the issue quickly, please include:
20
-
21
- - Description of the vulnerability
22
- - Steps to reproduce (if applicable)
23
- - Potential impact
24
- - Suggestions for fixing (if any)
25
-
26
- ### Process
27
-
28
- 1. I will acknowledge your report within 48 hours
29
- 2. I will assess the vulnerability
30
- 3. Work on a fix will begin depending on severity
31
- 4. After the fix is released, I will notify you and acknowledge your contribution (if you agree)
32
-
33
- Thank you for helping keep this project secure!
1
+ # Security Policy
2
+
3
+ ## Reporting a Vulnerability
4
+
5
+ I take the security of `@via-profit/ability` seriously. If you discover a security vulnerability, please report it responsibly.
6
+
7
+ ### How to Report a Vulnerability
8
+
9
+ **Please DO NOT create a public GitHub issue for security vulnerabilities.**
10
+
11
+ Instead, send the details directly to me:
12
+
13
+ - **Email**: [delhsmail@gmail.com](mailto:delhsmail@gmail.com)
14
+ - **Author**: Vasily Novosad
15
+ - **Timezone**: UTC+5 (for coordinating response time)
16
+
17
+ ### What to Include
18
+
19
+ To help me address the issue quickly, please include:
20
+
21
+ - Description of the vulnerability
22
+ - Steps to reproduce (if applicable)
23
+ - Potential impact
24
+ - Suggestions for fixing (if any)
25
+
26
+ ### Process
27
+
28
+ 1. I will acknowledge your report within 48 hours
29
+ 2. I will assess the vulnerability
30
+ 3. Work on a fix will begin depending on severity
31
+ 4. After the fix is released, I will notify you and acknowledge your contribution (if you agree)
32
+
33
+ Thank you for helping keep this project secure!
@@ -3,7 +3,7 @@ import AbilityMatch from './AbilityMatch';
3
3
  import AbilityCompare, { AbilityCompareCodeType } from './AbilityCompare';
4
4
  import AbilityPolicyEffect, { AbilityPolicyEffectCodeType } from './AbilityPolicyEffect';
5
5
  import { AbilityExplain } from './AbilityExplain';
6
- import { ResourceObject } from './AbilityParser';
6
+ import { ResourceObject } from './AbilityTypeGenerator';
7
7
  export type AbilityPolicyConfig = {
8
8
  readonly permission: string;
9
9
  readonly effect: AbilityPolicyEffectCodeType;
@@ -1,6 +1,6 @@
1
1
  import AbilityPolicy from './AbilityPolicy';
2
2
  import { AbilityResult } from './AbilityResult';
3
- import { ResourcesMap } from './AbilityParser';
3
+ import { ResourcesMap } from './AbilityTypeGenerator';
4
4
  import { AbilityCacheAdapter } from '../cache/AbilityCacheAdapter';
5
5
  export type AbilityResolverOptions = {
6
6
  readonly cache?: AbilityCacheAdapter | null;
@@ -1,5 +1,5 @@
1
1
  import { AbilityExplain } from './AbilityExplain';
2
- import { ResourceObject } from './AbilityParser';
2
+ import { ResourceObject } from './AbilityTypeGenerator';
3
3
  import AbilityPolicy from './AbilityPolicy';
4
4
  import AbilityPolicyEffect from './AbilityPolicyEffect';
5
5
  export declare class AbilityResult<Resource extends ResourceObject = Record<string, unknown>> {
@@ -1,7 +1,7 @@
1
1
  import AbilityRule, { AbilityRuleConfig } from './AbilityRule';
2
2
  import AbilityCompare, { AbilityCompareCodeType } from './AbilityCompare';
3
3
  import AbilityMatch from './AbilityMatch';
4
- import { ResourceObject } from './AbilityParser';
4
+ import { ResourceObject } from './AbilityTypeGenerator';
5
5
  export type AbilityRuleSetConfig = {
6
6
  readonly id?: string | null;
7
7
  readonly name?: string | null;
@@ -0,0 +1,55 @@
1
+ import AbilityPolicy from './AbilityPolicy';
2
+ export type Primitive = string | number | boolean | null | undefined;
3
+ export type NestedDict<T = Primitive> = {
4
+ [key: string]: NestedDict<T> | T;
5
+ };
6
+ export type ResourceObject = Record<string, unknown>;
7
+ export type ResourcesMap = Record<string, ResourceObject>;
8
+ export declare class AbilityTypeGenerator {
9
+ readonly policies: readonly AbilityPolicy[];
10
+ constructor(policies: readonly AbilityPolicy[]);
11
+ /**
12
+ * Generates TypeScript type definitions based on the provided policies.
13
+ * @returns A generated type definitions.
14
+ */
15
+ generateTypeDefs(): string;
16
+ /**
17
+ * Determines TypeScript type based on the rule
18
+ * @param rule - The rule to analyze
19
+ * @returns TypeScript type as string
20
+ */
21
+ private determineTypeFromRule;
22
+ /**
23
+ * Gets TypeScript type for array values
24
+ * @param resource - The resource value to analyze
25
+ * @returns TypeScript array type as string
26
+ */
27
+ private getArrayType;
28
+ /**
29
+ * Gets primitive TypeScript type for a value
30
+ * @param value - The value to analyze
31
+ * @returns TypeScript primitive type as string
32
+ */
33
+ private getPrimitiveType;
34
+ /**
35
+ * Builds nested structure from flat paths
36
+ * Example: 'user.profile.name' -> { user: { profile: { name: 'string' } } }
37
+ * @param flatStructure - Flat structure with dot notation paths
38
+ * @returns Nested object structure
39
+ */
40
+ private buildNestedStructure;
41
+ /**
42
+ * Formats type structure into a string
43
+ * @param structure - Nested type structure
44
+ * @returns Formatted TypeScript type definition string
45
+ */
46
+ private formatTypeDefinitions;
47
+ /**
48
+ * Recursively formats nested object
49
+ * @param obj - Object to format
50
+ * @param indent - Current indentation level
51
+ * @returns Formatted string
52
+ */
53
+ private formatNestedObject;
54
+ }
55
+ export default AbilityTypeGenerator;
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export * from './core/AbilityCompare';
3
3
  export * from './core/AbilityCondition';
4
4
  export * from './core/AbilityError';
5
5
  export * from './core/AbilityMatch';
6
- export * from './core/AbilityParser';
6
+ export * from './core/AbilityTypeGenerator';
7
7
  export * from './core/AbilityPolicy';
8
8
  export * from './core/AbilityPolicyEffect';
9
9
  export * from './core/AbilityResolver';
package/dist/index.js CHANGED
@@ -94,56 +94,20 @@ class AbilityMatch extends AbilityCode {
94
94
  static mismatch = new AbilityMatch('mismatch');
95
95
  }
96
96
 
97
- class AbilityParser {
98
- /**
99
- * Sets a value in a nested object structure based on a dot/bracket notation path.
100
- * @param object - The target object to modify.
101
- * @param path - The path to the property in dot/bracket notation.
102
- * @param value - The value to set at the specified path.
103
- */
104
- static setValueDotValue(object, path, value) {
105
- if (!path || path.trim().length === 0) {
106
- throw new AbilityParserError(`Invalid path provided on a [${path}]`);
107
- }
108
- const way = path.replace(/\[/g, '.').replace(/]/g, '').split('.');
109
- const last = way.pop();
110
- if (!last) {
111
- throw new AbilityParserError(`Invalid path provided on a [${path}]`);
112
- }
113
- const lastObj = way.reduce((acc, key, index, array) => {
114
- const currentValue = acc[key];
115
- const nextKey = array[index + 1];
116
- const shouldBeArray = nextKey !== undefined && isFinite(Number(nextKey));
117
- if (currentValue === undefined || currentValue === null) {
118
- // Create missing property
119
- const newValue = shouldBeArray ? [] : {};
120
- acc[key] = newValue;
121
- return newValue;
122
- }
123
- if (typeof currentValue !== 'object') {
124
- throw new AbilityParserError(`Cannot set property '${key}' on non-object value at path: ${path}`);
125
- }
126
- return currentValue;
127
- }, object);
128
- const existingValue = lastObj[last];
129
- if (existingValue !== undefined &&
130
- typeof existingValue === 'object' &&
131
- existingValue !== null &&
132
- !Array.isArray(existingValue)) {
133
- throw new AbilityParserError(`Cannot set primitive value on existing object at path: ${path}`);
134
- }
135
- lastObj[last] = value;
97
+ class AbilityTypeGenerator {
98
+ policies;
99
+ constructor(policies) {
100
+ this.policies = policies;
136
101
  }
137
102
  /**
138
103
  * Generates TypeScript type definitions based on the provided policies.
139
- * @param policies - An array of AbilityPolicy instances.
140
104
  * @returns A generated type definitions.
141
105
  */
142
- static generateTypeDefs(policies) {
106
+ generateTypeDefs() {
143
107
  // Structure to store types: { [action]: { [subjectPath]: type } }
144
108
  const typeStructure = {};
145
109
  // Iterate through all policies
146
- policies.forEach(policy => {
110
+ this.policies.forEach(policy => {
147
111
  const action = policy.permission;
148
112
  // Initialize object for action if it doesn't exist
149
113
  if (!typeStructure[action]) {
@@ -175,7 +139,7 @@ class AbilityParser {
175
139
  * @param rule - The rule to analyze
176
140
  * @returns TypeScript type as string
177
141
  */
178
- static determineTypeFromRule(rule) {
142
+ determineTypeFromRule(rule) {
179
143
  // Numeric comparisons - always number
180
144
  if (rule.condition.isEqual(AbilityCondition.greater_than) ||
181
145
  rule.condition.isEqual(AbilityCondition.less_than) ||
@@ -200,7 +164,7 @@ class AbilityParser {
200
164
  * @param resource - The resource value to analyze
201
165
  * @returns TypeScript array type as string
202
166
  */
203
- static getArrayType(resource) {
167
+ getArrayType(resource) {
204
168
  if (Array.isArray(resource)) {
205
169
  if (resource.length === 0)
206
170
  return 'any[]';
@@ -209,18 +173,18 @@ class AbilityParser {
209
173
  const elementType = elementTypes.size === 1
210
174
  ? Array.from(elementTypes)[0]
211
175
  : `(${Array.from(elementTypes).join(' | ')})`;
212
- return `${elementType}[]`;
176
+ return `readonly ${elementType}[]`;
213
177
  }
214
178
  // If resource is not an array but condition is in/not_in,
215
179
  // it expects an array of such elements
216
- return `${this.getPrimitiveType(resource)}[]`;
180
+ return `readonly ${this.getPrimitiveType(resource)}[]`;
217
181
  }
218
182
  /**
219
183
  * Gets primitive TypeScript type for a value
220
184
  * @param value - The value to analyze
221
185
  * @returns TypeScript primitive type as string
222
186
  */
223
- static getPrimitiveType(value) {
187
+ getPrimitiveType(value) {
224
188
  if (value === null)
225
189
  return 'null';
226
190
  if (value === undefined)
@@ -247,7 +211,7 @@ class AbilityParser {
247
211
  * @param flatStructure - Flat structure with dot notation paths
248
212
  * @returns Nested object structure
249
213
  */
250
- static buildNestedStructure(flatStructure) {
214
+ buildNestedStructure(flatStructure) {
251
215
  const result = {};
252
216
  Object.entries(flatStructure).forEach(([action, paths]) => {
253
217
  result[action] = {};
@@ -279,10 +243,9 @@ class AbilityParser {
279
243
  * @param structure - Nested type structure
280
244
  * @returns Formatted TypeScript type definition string
281
245
  */
282
- static formatTypeDefinitions(structure) {
246
+ formatTypeDefinitions(structure) {
283
247
  let output = '// Automatically generated by via-profit/ability\n';
284
248
  output += '// Do not edit manually\n';
285
- output += '\n/* eslint-disable */\n\n';
286
249
  output += 'export type Resources = {\n';
287
250
  // Sort actions for stable output
288
251
  const sortedActions = Object.keys(structure).sort();
@@ -300,7 +263,7 @@ class AbilityParser {
300
263
  * @param indent - Current indentation level
301
264
  * @returns Formatted string
302
265
  */
303
- static formatNestedObject(obj, indent) {
266
+ formatNestedObject(obj, indent) {
304
267
  const spaces = ' '.repeat(indent);
305
268
  let output = '';
306
269
  // Sort keys for stable output
@@ -587,8 +550,12 @@ class AbilityResolver {
587
550
  async enforce(permission, resource, environment) {
588
551
  const result = await this.resolve(permission, resource, environment);
589
552
  if (result.isDenied()) {
590
- const policyName = result.getLastMatchedPolicy()?.name?.toString() || 'unknown';
591
- throw new AbilityError(`Permission denied by policy "${policyName}"`);
553
+ const lastPolicy = result.getLastMatchedPolicy();
554
+ if (lastPolicy) {
555
+ throw new AbilityError(`Permission denied by policy "${lastPolicy.name.toString()}"`);
556
+ }
557
+ // No policy matched → implicit deny
558
+ throw new AbilityError(`Permission denied: no matching policy found (implicit deny)`);
592
559
  }
593
560
  }
594
561
  /**
@@ -2235,7 +2202,6 @@ exports.AbilityExplainRuleSet = AbilityExplainRuleSet;
2235
2202
  exports.AbilityInMemoryCache = AbilityInMemoryCache;
2236
2203
  exports.AbilityJSONParser = AbilityJSONParser;
2237
2204
  exports.AbilityMatch = AbilityMatch;
2238
- exports.AbilityParser = AbilityParser;
2239
2205
  exports.AbilityParserError = AbilityParserError;
2240
2206
  exports.AbilityPolicy = AbilityPolicy;
2241
2207
  exports.AbilityPolicyEffect = AbilityPolicyEffect;
@@ -2243,3 +2209,4 @@ exports.AbilityResolver = AbilityResolver;
2243
2209
  exports.AbilityResult = AbilityResult;
2244
2210
  exports.AbilityRule = AbilityRule;
2245
2211
  exports.AbilityRuleSet = AbilityRuleSet;
2212
+ exports.AbilityTypeGenerator = AbilityTypeGenerator;
@@ -1,5 +1,5 @@
1
1
  import AbilityPolicy from '../../core/AbilityPolicy';
2
- import { ResourceObject } from '../../core/AbilityParser';
2
+ import { ResourceObject } from '../../core/AbilityTypeGenerator';
3
3
  /**
4
4
  * Parser for the Ability DSL.
5
5
  *
@@ -1,6 +1,6 @@
1
1
  import { AbilityRule, AbilityRuleConfig } from '../../core/AbilityRule';
2
2
  import { AbilityRuleSet, AbilityRuleSetConfig } from '../../core/AbilityRuleSet';
3
- import { ResourceObject } from '../../core/AbilityParser';
3
+ import { ResourceObject } from '../../core/AbilityTypeGenerator';
4
4
  import { AbilityPolicy, AbilityPolicyConfig } from '../../core/AbilityPolicy';
5
5
  export declare class AbilityJSONParser {
6
6
  /**
package/package.json CHANGED
@@ -1,73 +1,73 @@
1
- {
2
- "name": "@via-profit/ability",
3
- "support": "https://via-profit.ru",
4
- "version": "3.2.0",
5
- "description": "Via-Profit Ability service",
6
- "keywords": [
7
- "ability",
8
- "access",
9
- "via-profit"
10
- ],
11
- "main": "./dist/index.js",
12
- "engines": {
13
- "node": ">= 17.0.0",
14
- "npm": ">= 8.19.3"
15
- },
16
- "files": [
17
- "dist",
18
- "README.md",
19
- "LICENSE",
20
- "CHANGELOG.md",
21
- "CONTRIBUTING.md",
22
- "SECURITY.md"
23
- ],
24
- "scripts": {
25
- "build": "rollup -c",
26
- "build:dev": "cross-env NODE_ENV=development rollup -c -w",
27
- "bench": "npm run build && node ./bench/benchmark.js",
28
- "test": "jest",
29
- "lint": "tsc --noEmit && eslint --fix .",
30
- "pretty": "prettier --write ./src"
31
- },
32
- "repository": {
33
- "type": "git",
34
- "url": "https://github.com/via-profit/ability.git"
35
- },
36
- "author": {
37
- "name": "Via Profit",
38
- "url": "https://dev.via-profit.ru"
39
- },
40
- "contributors": [
41
- "Vasily Novosad <delhsmail@gmail.com>",
42
- "Pavel Natalin <trubonru@gmail.com>"
43
- ],
44
- "license": "MIT",
45
- "devDependencies": {
46
- "@eslint/js": "^9.13.0",
47
- "@jagi/jest-transform-graphql": "^1.0.2",
48
- "@jest/types": "^29.6.3",
49
- "@rollup/plugin-alias": "^6.0.0",
50
- "@rollup/plugin-commonjs": "^29.0.2",
51
- "@rollup/plugin-node-resolve": "^16.0.3",
52
- "@rollup/plugin-typescript": "^12.3.0",
53
- "@types/jest": "^29.5.13",
54
- "@types/node": "^22.7.7",
55
- "@types/nodemon": "^1.19.6",
56
- "concurrently": "^9.0.1",
57
- "cross-env": "^7.0.3",
58
- "eslint": "^9.13.0",
59
- "globals": "^15.11.0",
60
- "jest": "^29.7.0",
61
- "jest-transform-graphql": "^2.1.0",
62
- "nodemon": "^3.1.7",
63
- "prettier": "^3.3.3",
64
- "rollup": "^4.60.0",
65
- "rollup-plugin-copy": "^3.5.0",
66
- "tinybench": "^6.0.0",
67
- "ts-jest": "^29.2.5",
68
- "ts-loader": "^9.5.1",
69
- "ts-node": "^10.9.2",
70
- "typescript": "^5.6.3",
71
- "typescript-eslint": "^8.10.0"
72
- }
73
- }
1
+ {
2
+ "name": "@via-profit/ability",
3
+ "support": "https://via-profit.ru",
4
+ "version": "3.3.0",
5
+ "description": "Via-Profit Ability service",
6
+ "keywords": [
7
+ "ability",
8
+ "access",
9
+ "via-profit"
10
+ ],
11
+ "main": "./dist/index.js",
12
+ "engines": {
13
+ "node": ">= 17.0.0",
14
+ "npm": ">= 8.19.3"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE",
20
+ "CHANGELOG.md",
21
+ "CONTRIBUTING.md",
22
+ "SECURITY.md"
23
+ ],
24
+ "scripts": {
25
+ "build": "rollup -c",
26
+ "build:dev": "cross-env NODE_ENV=development rollup -c -w",
27
+ "bench": "npm run build && node ./bench/benchmark.js",
28
+ "test": "jest",
29
+ "lint": "tsc --noEmit && eslint --fix .",
30
+ "pretty": "prettier --write ./src"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/via-profit/ability.git"
35
+ },
36
+ "author": {
37
+ "name": "Via Profit",
38
+ "url": "https://dev.via-profit.ru"
39
+ },
40
+ "contributors": [
41
+ "Vasily Novosad <delhsmail@gmail.com>",
42
+ "Pavel Natalin <trubonru@gmail.com>"
43
+ ],
44
+ "license": "MIT",
45
+ "devDependencies": {
46
+ "@eslint/js": "^9.13.0",
47
+ "@jagi/jest-transform-graphql": "^1.0.2",
48
+ "@jest/types": "^29.6.3",
49
+ "@rollup/plugin-alias": "^6.0.0",
50
+ "@rollup/plugin-commonjs": "^29.0.2",
51
+ "@rollup/plugin-node-resolve": "^16.0.3",
52
+ "@rollup/plugin-typescript": "^12.3.0",
53
+ "@types/jest": "^29.5.13",
54
+ "@types/node": "^22.7.7",
55
+ "@types/nodemon": "^1.19.6",
56
+ "concurrently": "^9.0.1",
57
+ "cross-env": "^7.0.3",
58
+ "eslint": "^9.13.0",
59
+ "globals": "^15.11.0",
60
+ "jest": "^29.7.0",
61
+ "jest-transform-graphql": "^2.1.0",
62
+ "nodemon": "^3.1.7",
63
+ "prettier": "^3.3.3",
64
+ "rollup": "^4.60.0",
65
+ "rollup-plugin-copy": "^3.5.0",
66
+ "tinybench": "^6.0.0",
67
+ "ts-jest": "^29.2.5",
68
+ "ts-loader": "^9.5.1",
69
+ "ts-node": "^10.9.2",
70
+ "typescript": "^5.6.3",
71
+ "typescript-eslint": "^8.10.0"
72
+ }
73
+ }