@via-profit/ability 3.6.5 → 3.7.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.
- package/README.md +245 -1165
- package/dist/index.cjs +100 -45
- package/dist/index.d.ts +19 -6
- package/dist/index.js +100 -45
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -30,6 +30,8 @@ function brand$3(code) {
|
|
|
30
30
|
}
|
|
31
31
|
const AbilityCondition = {
|
|
32
32
|
equals: brand$3('='),
|
|
33
|
+
defined: brand$3('defined'),
|
|
34
|
+
not_defined: brand$3('not_defined'),
|
|
33
35
|
not_equals: brand$3('<>'),
|
|
34
36
|
greater_than: brand$3('>'),
|
|
35
37
|
less_than: brand$3('<'),
|
|
@@ -62,6 +64,8 @@ function fromLiteral(literal) {
|
|
|
62
64
|
length_equals: AbilityCondition.length_equals,
|
|
63
65
|
always: AbilityCondition.always,
|
|
64
66
|
never: AbilityCondition.never,
|
|
67
|
+
defined: AbilityCondition.defined,
|
|
68
|
+
not_defined: AbilityCondition.not_defined,
|
|
65
69
|
};
|
|
66
70
|
const value = map[literal];
|
|
67
71
|
if (!value) {
|
|
@@ -102,6 +106,10 @@ function toLiteral(cond) {
|
|
|
102
106
|
return 'always';
|
|
103
107
|
case AbilityCondition.never:
|
|
104
108
|
return 'never';
|
|
109
|
+
case AbilityCondition.defined:
|
|
110
|
+
return 'defined';
|
|
111
|
+
case AbilityCondition.not_defined:
|
|
112
|
+
return 'not_defined';
|
|
105
113
|
default:
|
|
106
114
|
return 'never';
|
|
107
115
|
}
|
|
@@ -124,15 +132,6 @@ const AbilityMatch = {
|
|
|
124
132
|
disabled: brand$2('disabled'),
|
|
125
133
|
};
|
|
126
134
|
|
|
127
|
-
const colors = {
|
|
128
|
-
reset: '\x1b[0m',
|
|
129
|
-
green: '\x1b[32m',
|
|
130
|
-
red: '\x1b[31m',
|
|
131
|
-
blue: '\x1b[34m',
|
|
132
|
-
yellow: '\x1b[33m',
|
|
133
|
-
white: '\x1b[37m',
|
|
134
|
-
gray: '\x1b[90m',
|
|
135
|
-
};
|
|
136
135
|
class AbilityExplain {
|
|
137
136
|
type;
|
|
138
137
|
children;
|
|
@@ -148,27 +147,36 @@ class AbilityExplain {
|
|
|
148
147
|
}
|
|
149
148
|
toString(indentPrefix = '', isLast = true) {
|
|
150
149
|
const isMatch = this.match === AbilityMatch.match;
|
|
151
|
-
const
|
|
150
|
+
const isMismatch = this.match === AbilityMatch.mismatch;
|
|
151
|
+
const isPending = this.match === AbilityMatch.pending;
|
|
152
|
+
// const isDisabled = this.match === AbilityMatch.disabled;
|
|
153
|
+
const mark = isMatch
|
|
154
|
+
? `<match ✓>`
|
|
155
|
+
: isMismatch
|
|
156
|
+
? `<mismatch ✗>`
|
|
157
|
+
: isPending
|
|
158
|
+
? `<pending …>`
|
|
159
|
+
: `<disabled ⊘>`;
|
|
152
160
|
let label;
|
|
153
161
|
switch (this.type) {
|
|
154
162
|
case 'policy':
|
|
155
|
-
label =
|
|
163
|
+
label = `POLICY`;
|
|
156
164
|
break;
|
|
157
165
|
case 'ruleSet':
|
|
158
|
-
label =
|
|
166
|
+
label = `RULESET`;
|
|
159
167
|
break;
|
|
160
168
|
default:
|
|
161
|
-
label =
|
|
169
|
+
label = `RULE`;
|
|
162
170
|
}
|
|
163
171
|
const branch = indentPrefix.length === 0
|
|
164
172
|
? ''
|
|
165
173
|
: isLast
|
|
166
|
-
?
|
|
167
|
-
:
|
|
174
|
+
? `└─ `
|
|
175
|
+
: `├─ `;
|
|
168
176
|
let out = `${indentPrefix}${branch}${label} ${this.name} — ${mark}`;
|
|
169
177
|
if (this.debugInfo)
|
|
170
|
-
out += `
|
|
171
|
-
const nextIndent = indentPrefix + (isLast ? ' ' :
|
|
178
|
+
out += ` (${this.debugInfo})`;
|
|
179
|
+
const nextIndent = indentPrefix + (isLast ? ' ' : `│ `);
|
|
172
180
|
this.children.forEach((child, idx) => {
|
|
173
181
|
out += '\n' + child.toString(nextIndent, idx === this.children.length - 1);
|
|
174
182
|
});
|
|
@@ -181,6 +189,7 @@ class AbilityExplainRule extends AbilityExplain {
|
|
|
181
189
|
type: 'rule',
|
|
182
190
|
match: rule.state,
|
|
183
191
|
name: rule.name,
|
|
192
|
+
debugInfo: `${rule.subject} ${rule.condition} ${JSON.stringify(rule.resource)}`,
|
|
184
193
|
});
|
|
185
194
|
}
|
|
186
195
|
}
|
|
@@ -219,11 +228,13 @@ class AbilityResult {
|
|
|
219
228
|
* Useful for debugging, logging, or building UI tools that visualize permission logic.
|
|
220
229
|
*/
|
|
221
230
|
explain() {
|
|
222
|
-
|
|
231
|
+
const resMarker = this.strategy.isDenied() ? '== DENIED==' : '== ALLOWED ==';
|
|
232
|
+
const policiesExplain = this.strategy.policies
|
|
223
233
|
.map(policy => {
|
|
224
234
|
return new AbilityExplainPolicy(policy).toString();
|
|
225
235
|
})
|
|
226
236
|
.join('\n');
|
|
237
|
+
return `${resMarker}\n${policiesExplain}\n`;
|
|
227
238
|
}
|
|
228
239
|
decisive() {
|
|
229
240
|
return this.strategy.decisivePolicy();
|
|
@@ -243,7 +254,17 @@ class AbilityResult {
|
|
|
243
254
|
};
|
|
244
255
|
}
|
|
245
256
|
|
|
257
|
+
function brand$1(code) {
|
|
258
|
+
return code;
|
|
259
|
+
}
|
|
260
|
+
const AbilityPolicyEffect = {
|
|
261
|
+
deny: brand$1('deny'),
|
|
262
|
+
permit: brand$1('permit'),
|
|
263
|
+
};
|
|
264
|
+
|
|
246
265
|
class AbilityResolver {
|
|
266
|
+
onDeny;
|
|
267
|
+
onAllow;
|
|
247
268
|
StrategyClass;
|
|
248
269
|
policyEntries;
|
|
249
270
|
constructor(
|
|
@@ -252,6 +273,8 @@ class AbilityResolver {
|
|
|
252
273
|
*/
|
|
253
274
|
policyOrListOfPolicies, strategy, options = {}) {
|
|
254
275
|
const policies = this.toArray(policyOrListOfPolicies);
|
|
276
|
+
this.onDeny = options.onDeny;
|
|
277
|
+
this.onAllow = options.onAllow;
|
|
255
278
|
const filtered = options.tags
|
|
256
279
|
? policies.filter(p => p.tags.some(tag => options.tags.includes(tag)))
|
|
257
280
|
: policies;
|
|
@@ -289,11 +312,19 @@ class AbilityResolver {
|
|
|
289
312
|
// 3. Use strategy
|
|
290
313
|
const strategy = new this.StrategyClass(filteredPolicies);
|
|
291
314
|
const effect = strategy.evaluate();
|
|
292
|
-
|
|
315
|
+
const result = new AbilityResult(effect, strategy);
|
|
316
|
+
if (effect === AbilityPolicyEffect.deny && this.onDeny) {
|
|
317
|
+
this.onDeny(result);
|
|
318
|
+
}
|
|
319
|
+
if (effect === AbilityPolicyEffect.permit && this.onAllow) {
|
|
320
|
+
this.onAllow(result);
|
|
321
|
+
}
|
|
322
|
+
return result;
|
|
293
323
|
}
|
|
294
|
-
enforce(permission, resource, environment) {
|
|
324
|
+
enforce(permission, resource, environment, options) {
|
|
295
325
|
const result = this.resolve(permission, resource, environment);
|
|
296
326
|
if (result.isDenied()) {
|
|
327
|
+
options?.onDeny && options?.onDeny(result);
|
|
297
328
|
throw new AbilityError(`Permission denied`);
|
|
298
329
|
}
|
|
299
330
|
}
|
|
@@ -414,7 +445,7 @@ class AbilityTypeGenerator {
|
|
|
414
445
|
environmentStructure[action] = {};
|
|
415
446
|
}
|
|
416
447
|
const existingEnvType = environmentStructure[action][envPath];
|
|
417
|
-
const targetType = ruleType;
|
|
448
|
+
const targetType = ruleType;
|
|
418
449
|
if (existingEnvType && existingEnvType !== targetType) {
|
|
419
450
|
environmentStructure[action][envPath] = `${existingEnvType} | ${targetType}`;
|
|
420
451
|
}
|
|
@@ -1002,14 +1033,6 @@ class AbilityPolicy {
|
|
|
1002
1033
|
}
|
|
1003
1034
|
}
|
|
1004
1035
|
|
|
1005
|
-
function brand$1(code) {
|
|
1006
|
-
return code;
|
|
1007
|
-
}
|
|
1008
|
-
const AbilityPolicyEffect = {
|
|
1009
|
-
deny: brand$1('deny'),
|
|
1010
|
-
permit: brand$1('permit'),
|
|
1011
|
-
};
|
|
1012
|
-
|
|
1013
1036
|
/**
|
|
1014
1037
|
* Represents a rule that defines a condition to be checked against a subject and resource.
|
|
1015
1038
|
*/
|
|
@@ -1061,6 +1084,8 @@ class AbilityRule {
|
|
|
1061
1084
|
static valueLen = (v) => this.isString(v) || Array.isArray(v) ? v.length : null;
|
|
1062
1085
|
static operatorHandlers = {
|
|
1063
1086
|
[toLiteral(AbilityCondition.always)]: () => true,
|
|
1087
|
+
[toLiteral(AbilityCondition.defined)]: (a) => typeof a !== 'undefined',
|
|
1088
|
+
[toLiteral(AbilityCondition.not_defined)]: (a) => typeof a === 'undefined',
|
|
1064
1089
|
[toLiteral(AbilityCondition.never)]: () => false,
|
|
1065
1090
|
[toLiteral(AbilityCondition.equals)]: (a, b) => a === b,
|
|
1066
1091
|
[toLiteral(AbilityCondition.not_equals)]: (a, b) => a !== b,
|
|
@@ -1600,6 +1625,7 @@ const TokenTypes = {
|
|
|
1600
1625
|
NULL: brand('NULL'),
|
|
1601
1626
|
EQ_NULL: brand('EQ_NULL'),
|
|
1602
1627
|
NOT_EQ_NULL: brand('NOT_EQ_NULL'),
|
|
1628
|
+
DEFINED: brand('DEFINED'),
|
|
1603
1629
|
NOT_EQ: brand('NOT_EQ'),
|
|
1604
1630
|
LEN_GT: brand('LEN_GT'),
|
|
1605
1631
|
LEN_LT: brand('LEN_LT'),
|
|
@@ -1650,9 +1676,11 @@ class AbilityDSLLexer {
|
|
|
1650
1676
|
'true',
|
|
1651
1677
|
'false',
|
|
1652
1678
|
'null',
|
|
1679
|
+
'defined',
|
|
1653
1680
|
'contains',
|
|
1654
1681
|
'includes',
|
|
1655
1682
|
'length',
|
|
1683
|
+
'len',
|
|
1656
1684
|
'has',
|
|
1657
1685
|
'in',
|
|
1658
1686
|
'gt',
|
|
@@ -1866,11 +1894,11 @@ class AbilityDSLLexer {
|
|
|
1866
1894
|
const startLine = this.line;
|
|
1867
1895
|
const startColumn = this.column;
|
|
1868
1896
|
const start = this.pos;
|
|
1869
|
-
//
|
|
1897
|
+
// First segment
|
|
1870
1898
|
while (!this.isAtEnd() && /[a-zA-Z0-9_*]/.test(this.peek())) {
|
|
1871
1899
|
this.advance();
|
|
1872
1900
|
}
|
|
1873
|
-
//
|
|
1901
|
+
// dots segments
|
|
1874
1902
|
while (!this.isAtEnd() && this.peek() === '.') {
|
|
1875
1903
|
this.advance(); // dot
|
|
1876
1904
|
if (!/[a-zA-Z_*]/.test(this.peek())) {
|
|
@@ -1887,7 +1915,7 @@ class AbilityDSLLexer {
|
|
|
1887
1915
|
if (word === 'never') {
|
|
1888
1916
|
return new AbilityDSLToken(TokenTypes.NEVER, word, startLine, startColumn);
|
|
1889
1917
|
}
|
|
1890
|
-
//
|
|
1918
|
+
// (identifier or permission)
|
|
1891
1919
|
if (word.includes('.')) {
|
|
1892
1920
|
const last = this.tokens[this.tokens.length - 1];
|
|
1893
1921
|
if (last?.type === TokenTypes.EFFECT) {
|
|
@@ -1897,16 +1925,13 @@ class AbilityDSLLexer {
|
|
|
1897
1925
|
}
|
|
1898
1926
|
return new AbilityDSLToken(TokenTypes.IDENTIFIER, word, startLine, startColumn);
|
|
1899
1927
|
}
|
|
1900
|
-
// Ключевые слова
|
|
1901
1928
|
if (this.keywords.has(word)) {
|
|
1902
|
-
// Эффекты
|
|
1903
1929
|
if (word === 'permit' || word === 'allow') {
|
|
1904
1930
|
return new AbilityDSLToken(TokenTypes.EFFECT, 'permit', startLine, startColumn);
|
|
1905
1931
|
}
|
|
1906
1932
|
if (word === 'deny' || word === 'forbidden') {
|
|
1907
1933
|
return new AbilityDSLToken(TokenTypes.EFFECT, 'deny', startLine, startColumn);
|
|
1908
1934
|
}
|
|
1909
|
-
// Групповые ключевые слова
|
|
1910
1935
|
if (word === 'all') {
|
|
1911
1936
|
return new AbilityDSLToken(TokenTypes.ALL, word, startLine, startColumn);
|
|
1912
1937
|
}
|
|
@@ -1919,13 +1944,15 @@ class AbilityDSLLexer {
|
|
|
1919
1944
|
if (word === 'if') {
|
|
1920
1945
|
return new AbilityDSLToken(TokenTypes.IF, word, startLine, startColumn);
|
|
1921
1946
|
}
|
|
1922
|
-
// Булевы и null
|
|
1923
1947
|
if (word === 'true' || word === 'false') {
|
|
1924
1948
|
return new AbilityDSLToken(TokenTypes.BOOLEAN, word, startLine, startColumn);
|
|
1925
1949
|
}
|
|
1926
1950
|
if (word === 'null') {
|
|
1927
1951
|
return new AbilityDSLToken(TokenTypes.NULL, word, startLine, startColumn);
|
|
1928
1952
|
}
|
|
1953
|
+
if (word === 'defined') {
|
|
1954
|
+
return new AbilityDSLToken(TokenTypes.DEFINED, word, startLine, startColumn);
|
|
1955
|
+
}
|
|
1929
1956
|
if (word === 'except') {
|
|
1930
1957
|
return new AbilityDSLToken(TokenTypes.EXCEPT, word, startLine, startColumn);
|
|
1931
1958
|
}
|
|
@@ -2077,7 +2104,8 @@ class AbilityDSLTokenStream {
|
|
|
2077
2104
|
if (this.eof()) {
|
|
2078
2105
|
return false;
|
|
2079
2106
|
}
|
|
2080
|
-
|
|
2107
|
+
const p = this.peek().type;
|
|
2108
|
+
return p === type;
|
|
2081
2109
|
}
|
|
2082
2110
|
match(type) {
|
|
2083
2111
|
if (this.check(type)) {
|
|
@@ -2513,6 +2541,7 @@ class AbilityDSLParser {
|
|
|
2513
2541
|
const operatorConsumesValue = operator !== TokenTypes.EQ_NULL &&
|
|
2514
2542
|
operator !== TokenTypes.NOT_EQ_NULL &&
|
|
2515
2543
|
operator !== TokenTypes.NULL &&
|
|
2544
|
+
operator !== TokenTypes.DEFINED &&
|
|
2516
2545
|
operator !== TokenTypes.ALWAYS &&
|
|
2517
2546
|
operator !== TokenTypes.NEVER;
|
|
2518
2547
|
if (operatorConsumesValue) {
|
|
@@ -2565,42 +2594,46 @@ class AbilityDSLParser {
|
|
|
2565
2594
|
this.stream.reset();
|
|
2566
2595
|
// "length equals"
|
|
2567
2596
|
this.stream.mark();
|
|
2568
|
-
if (this.matchWord('length') && this.matchWord('equals')) {
|
|
2597
|
+
if ((this.matchWord('length') || this.matchWord('len')) && this.matchWord('equals')) {
|
|
2569
2598
|
this.stream.commit();
|
|
2570
2599
|
return { condition: AbilityCondition.length_equals, operator: TokenTypes.LEN_EQ };
|
|
2571
2600
|
}
|
|
2572
2601
|
this.stream.reset();
|
|
2573
2602
|
// "length ="
|
|
2574
2603
|
this.stream.mark();
|
|
2575
|
-
if (this.matchWord('length') && this.matchSymbol('=')) {
|
|
2604
|
+
if ((this.matchWord('length') || this.matchWord('len')) && this.matchSymbol('=')) {
|
|
2576
2605
|
this.stream.commit();
|
|
2577
2606
|
return { condition: AbilityCondition.length_equals, operator: TokenTypes.LEN_EQ };
|
|
2578
2607
|
}
|
|
2579
2608
|
this.stream.reset();
|
|
2580
2609
|
// "length greater than"
|
|
2581
2610
|
this.stream.mark();
|
|
2582
|
-
if (this.matchWord('length')
|
|
2611
|
+
if ((this.matchWord('length') || this.matchWord('len')) &&
|
|
2612
|
+
this.matchWord('greater') &&
|
|
2613
|
+
this.matchWord('than')) {
|
|
2583
2614
|
this.stream.commit();
|
|
2584
2615
|
return { condition: AbilityCondition.length_greater_than, operator: TokenTypes.LEN_GT };
|
|
2585
2616
|
}
|
|
2586
2617
|
this.stream.reset();
|
|
2587
2618
|
// "length >"
|
|
2588
2619
|
this.stream.mark();
|
|
2589
|
-
if (this.matchWord('length') && this.matchSymbol('>')) {
|
|
2620
|
+
if ((this.matchWord('length') || this.matchWord('len')) && this.matchSymbol('>')) {
|
|
2590
2621
|
this.stream.commit();
|
|
2591
2622
|
return { condition: AbilityCondition.length_greater_than, operator: TokenTypes.LEN_GT };
|
|
2592
2623
|
}
|
|
2593
2624
|
this.stream.reset();
|
|
2594
2625
|
// "length less than"
|
|
2595
2626
|
this.stream.mark();
|
|
2596
|
-
if (this.matchWord('length')
|
|
2627
|
+
if ((this.matchWord('length') || this.matchWord('len')) &&
|
|
2628
|
+
this.matchWord('less') &&
|
|
2629
|
+
this.matchWord('than')) {
|
|
2597
2630
|
this.stream.commit();
|
|
2598
2631
|
return { condition: AbilityCondition.length_less_than, operator: TokenTypes.LEN_LT };
|
|
2599
2632
|
}
|
|
2600
2633
|
this.stream.reset();
|
|
2601
2634
|
// "length <"
|
|
2602
2635
|
this.stream.mark();
|
|
2603
|
-
if (this.matchWord('length') && this.matchSymbol('<')) {
|
|
2636
|
+
if ((this.matchWord('length') || this.matchWord('len')) && this.matchSymbol('<')) {
|
|
2604
2637
|
this.stream.commit();
|
|
2605
2638
|
return { condition: AbilityCondition.length_less_than, operator: TokenTypes.LEN_LT };
|
|
2606
2639
|
}
|
|
@@ -2728,6 +2761,20 @@ class AbilityDSLParser {
|
|
|
2728
2761
|
}
|
|
2729
2762
|
}
|
|
2730
2763
|
this.stream.reset();
|
|
2764
|
+
// is defined
|
|
2765
|
+
this.stream.mark();
|
|
2766
|
+
if (this.matchWord('is') && this.matchWord('defined')) {
|
|
2767
|
+
this.stream.commit();
|
|
2768
|
+
return { condition: AbilityCondition.defined, operator: TokenTypes.DEFINED };
|
|
2769
|
+
}
|
|
2770
|
+
this.stream.reset();
|
|
2771
|
+
// is not defined
|
|
2772
|
+
this.stream.mark();
|
|
2773
|
+
if (this.matchWord('is') && this.matchWord('not') && this.matchWord('defined')) {
|
|
2774
|
+
this.stream.commit();
|
|
2775
|
+
return { condition: AbilityCondition.not_defined, operator: TokenTypes.DEFINED };
|
|
2776
|
+
}
|
|
2777
|
+
this.stream.reset();
|
|
2731
2778
|
// Single token (symbol or keyword)
|
|
2732
2779
|
const token = this.stream.peek();
|
|
2733
2780
|
if (token.type !== TokenTypes.SYMBOL &&
|
|
@@ -2799,7 +2846,8 @@ class AbilityDSLParser {
|
|
|
2799
2846
|
if ((token.type === TokenTypes.KEYWORD ||
|
|
2800
2847
|
token.type === TokenTypes.IDENTIFIER ||
|
|
2801
2848
|
token.type === TokenTypes.ALWAYS ||
|
|
2802
|
-
token.type === TokenTypes.NEVER
|
|
2849
|
+
token.type === TokenTypes.NEVER ||
|
|
2850
|
+
token.type === TokenTypes.DEFINED) &&
|
|
2803
2851
|
token.value === word) {
|
|
2804
2852
|
this.stream.next();
|
|
2805
2853
|
return true;
|
|
@@ -2837,6 +2885,11 @@ class AbilityDSLParser {
|
|
|
2837
2885
|
this.stream.syntaxError(`Unexpected ${token.type} in value position`, token);
|
|
2838
2886
|
}
|
|
2839
2887
|
this.stream.next();
|
|
2888
|
+
// if (token.type === TokenTypes.IDENTIFIER) {
|
|
2889
|
+
// this.stream.next();
|
|
2890
|
+
//
|
|
2891
|
+
// return this.parseValue();
|
|
2892
|
+
// }
|
|
2840
2893
|
// CHECK THIS SWITCH COMPARE
|
|
2841
2894
|
switch (token.type) {
|
|
2842
2895
|
case TokenTypes.STRING:
|
|
@@ -2847,8 +2900,10 @@ class AbilityDSLParser {
|
|
|
2847
2900
|
return token.value === 'true';
|
|
2848
2901
|
case TokenTypes.NULL:
|
|
2849
2902
|
return null;
|
|
2903
|
+
case TokenTypes.DEFINED:
|
|
2904
|
+
return typeof token.value !== 'undefined';
|
|
2850
2905
|
case TokenTypes.IDENTIFIER:
|
|
2851
|
-
return
|
|
2906
|
+
return null;
|
|
2852
2907
|
default: {
|
|
2853
2908
|
this.stream.syntaxError(`Unexpected value token "${token.value}"`, token, [
|
|
2854
2909
|
TokenTypes.KEYWORD,
|
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.
|
|
4
|
+
"version": "3.7.1",
|
|
5
5
|
"description": "Via-Profit Ability service",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"ability",
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"bench": "npm run build && node ./bench/benchmark.js",
|
|
35
35
|
"test": "jest",
|
|
36
36
|
"lint": "tsc --noEmit && eslint --fix .",
|
|
37
|
-
"pretty": "prettier --write ./src"
|
|
37
|
+
"pretty": "prettier --write ./src",
|
|
38
|
+
"postinstall": "node scripts/postinstall.js"
|
|
38
39
|
},
|
|
39
40
|
"repository": {
|
|
40
41
|
"type": "git",
|