@via-profit/ability 2.0.0-rc.7 → 2.1.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.
@@ -12,9 +12,12 @@
12
12
  Object.defineProperty(exports, "__esModule", ({ value: true }));
13
13
  exports.AbilityCode = void 0;
14
14
  class AbilityCode {
15
- code;
15
+ _code;
16
16
  constructor(code) {
17
- this.code = code;
17
+ this._code = code;
18
+ }
19
+ get code() {
20
+ return this._code;
18
21
  }
19
22
  isEqual(compareWith) {
20
23
  return compareWith !== null && this.code === compareWith.code;
@@ -22,46 +25,11 @@ class AbilityCode {
22
25
  isNotEqual(compareWith) {
23
26
  return !this.isEqual(compareWith);
24
27
  }
25
- static fromLiteral(literal) {
26
- let Code = null;
27
- Object.keys(this).forEach(member => {
28
- if (member === literal) {
29
- Code = this[member];
30
- }
31
- });
32
- if (!Code) {
33
- throw new Error(`Mismatch error. The literal ${literal} can not be find as a member of this class`);
34
- }
35
- return new this(Code.code);
36
- }
37
28
  }
38
29
  exports.AbilityCode = AbilityCode;
39
30
  exports["default"] = AbilityCode;
40
31
 
41
32
 
42
- /***/ }),
43
-
44
- /***/ "./src/AbilityCompare.ts":
45
- /*!*******************************!*\
46
- !*** ./src/AbilityCompare.ts ***!
47
- \*******************************/
48
- /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
49
-
50
-
51
- var __importDefault = (this && this.__importDefault) || function (mod) {
52
- return (mod && mod.__esModule) ? mod : { "default": mod };
53
- };
54
- Object.defineProperty(exports, "__esModule", ({ value: true }));
55
- exports.AbilityCompare = void 0;
56
- const AbilityCode_1 = __importDefault(__webpack_require__(/*! ./AbilityCode */ "./src/AbilityCode.ts"));
57
- class AbilityCompare extends AbilityCode_1.default {
58
- static and = new AbilityCompare(0);
59
- static or = new AbilityCompare(1);
60
- }
61
- exports.AbilityCompare = AbilityCompare;
62
- exports["default"] = AbilityCompare;
63
-
64
-
65
33
  /***/ }),
66
34
 
67
35
  /***/ "./src/AbilityCondition.ts":
@@ -77,15 +45,33 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
77
45
  Object.defineProperty(exports, "__esModule", ({ value: true }));
78
46
  exports.AbilityCondition = void 0;
79
47
  const AbilityCode_1 = __importDefault(__webpack_require__(/*! ./AbilityCode */ "./src/AbilityCode.ts"));
48
+ const AbilityError_1 = __webpack_require__(/*! ~/AbilityError */ "./src/AbilityError.ts");
80
49
  class AbilityCondition extends AbilityCode_1.default {
81
- static EQUAL = new AbilityCondition('=');
82
- static NOT_EQUAL = new AbilityCondition('<>');
83
- static MORE_THAN = new AbilityCondition('>');
84
- static LESS_THAN = new AbilityCondition('<');
85
- static LESS_OR_EQUAL = new AbilityCondition('<=');
86
- static MORE_OR_EQUAL = new AbilityCondition('>=');
87
- static IN = new AbilityCondition('in');
88
- static NOT_IN = new AbilityCondition('not in');
50
+ static equal = new AbilityCondition('=');
51
+ static not_equal = new AbilityCondition('<>');
52
+ static more_than = new AbilityCondition('>');
53
+ static less_than = new AbilityCondition('<');
54
+ static less_or_equal = new AbilityCondition('<=');
55
+ static more_or_equal = new AbilityCondition('>=');
56
+ static in = new AbilityCondition('in');
57
+ static not_in = new AbilityCondition('not in');
58
+ static fromLiteral(literal) {
59
+ const code = AbilityCondition[literal]?.code || null;
60
+ if (code === null) {
61
+ throw new AbilityError_1.AbilityParserError(`Literal ${literal} does not found in AbilityCondition class`);
62
+ }
63
+ return new AbilityCondition(code);
64
+ }
65
+ get literal() {
66
+ const literal = Object.keys(AbilityCondition).find(member => {
67
+ const val = AbilityCondition[member];
68
+ return val.code === this.code;
69
+ });
70
+ if (typeof literal === 'undefined') {
71
+ throw new Error(`Literal value does not found in class AbilityCondition`);
72
+ }
73
+ return literal;
74
+ }
89
75
  }
90
76
  exports.AbilityCondition = AbilityCondition;
91
77
  exports["default"] = AbilityCondition;
@@ -146,245 +132,6 @@ exports.AbilityMatch = AbilityMatch;
146
132
  exports["default"] = AbilityMatch;
147
133
 
148
134
 
149
- /***/ }),
150
-
151
- /***/ "./src/AbilityPolicy.ts":
152
- /*!******************************!*\
153
- !*** ./src/AbilityPolicy.ts ***!
154
- \******************************/
155
- /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
156
-
157
-
158
- var __importDefault = (this && this.__importDefault) || function (mod) {
159
- return (mod && mod.__esModule) ? mod : { "default": mod };
160
- };
161
- Object.defineProperty(exports, "__esModule", ({ value: true }));
162
- exports.AbilityPolicy = void 0;
163
- const AbilityRuleSet_1 = __importDefault(__webpack_require__(/*! ./AbilityRuleSet */ "./src/AbilityRuleSet.ts"));
164
- const AbilityMatch_1 = __importDefault(__webpack_require__(/*! ./AbilityMatch */ "./src/AbilityMatch.ts"));
165
- const AbilityCompare_1 = __importDefault(__webpack_require__(/*! ./AbilityCompare */ "./src/AbilityCompare.ts"));
166
- const AbilityPolicyEffect_1 = __importDefault(__webpack_require__(/*! ./AbilityPolicyEffect */ "./src/AbilityPolicyEffect.ts"));
167
- class AbilityPolicy {
168
- matchState = AbilityMatch_1.default.pending;
169
- /**
170
- * List of rules
171
- */
172
- ruleSet = [];
173
- /**
174
- * Policy effect
175
- */
176
- effect;
177
- /**
178
- * Rules compare method.\
179
- * For the «and» method the rule will be permitted if all\
180
- * rules will be returns «permit» status and for the «or» - if\
181
- * one of the rules returns as «permit»
182
- */
183
- compareMethod = AbilityCompare_1.default.and;
184
- /**
185
- * Policy name
186
- */
187
- name;
188
- /**
189
- * Policy ID
190
- */
191
- id;
192
- /**
193
- * Soon
194
- */
195
- action;
196
- constructor(params) {
197
- const { name, id, action, effect } = params;
198
- this.name = name;
199
- this.id = id;
200
- this.action = action;
201
- this.effect = effect;
202
- }
203
- /**
204
- * Add rule set to the policy
205
- * @param ruleSet - The rule set to add
206
- */
207
- addRuleSet(ruleSet) {
208
- this.ruleSet.push(ruleSet);
209
- return this;
210
- }
211
- /**
212
- * Check if the policy is matched
213
- * @param resources - The resource to check
214
- */
215
- check(resources) {
216
- this.matchState = AbilityMatch_1.default.mismatch;
217
- if (!this.ruleSet.length) {
218
- return this.matchState;
219
- }
220
- const rulesetCheckStates = this.ruleSet.reduce((collect, ruleSet) => {
221
- return collect.concat(ruleSet.check(resources));
222
- }, []);
223
- if (AbilityCompare_1.default.and.isEqual(this.compareMethod)) {
224
- if (rulesetCheckStates.every(ruleState => AbilityMatch_1.default.match.isEqual(ruleState))) {
225
- this.matchState = AbilityMatch_1.default.match;
226
- }
227
- }
228
- if (AbilityCompare_1.default.or.isEqual(this.compareMethod)) {
229
- if (rulesetCheckStates.some(ruleState => AbilityMatch_1.default.match.isEqual(ruleState))) {
230
- this.matchState = AbilityMatch_1.default.match;
231
- }
232
- }
233
- return this.matchState;
234
- }
235
- /**
236
- * Parse the config JSON format to Policy class instance
237
- */
238
- static parse(config) {
239
- const { id, name, ruleSet, compareMethod, action, effect } = config;
240
- // Create the empty policy
241
- const policy = new AbilityPolicy({
242
- name,
243
- id,
244
- action,
245
- effect: new AbilityPolicyEffect_1.default(effect),
246
- });
247
- policy.compareMethod = AbilityCompare_1.default.fromLiteral(compareMethod);
248
- ruleSet.forEach(ruleSetConfig => {
249
- policy.addRuleSet(AbilityRuleSet_1.default.parse(ruleSetConfig));
250
- });
251
- return policy;
252
- }
253
- export() {
254
- return {
255
- id: this.id.toString(),
256
- name: this.name.toString(),
257
- compareMethod: this.compareMethod.code.toString(),
258
- ruleSet: this.ruleSet.map(rule => rule.export()),
259
- action: this.action,
260
- effect: this.effect.code,
261
- };
262
- }
263
- }
264
- exports.AbilityPolicy = AbilityPolicy;
265
- exports["default"] = AbilityPolicy;
266
-
267
-
268
- /***/ }),
269
-
270
- /***/ "./src/AbilityPolicyEffect.ts":
271
- /*!************************************!*\
272
- !*** ./src/AbilityPolicyEffect.ts ***!
273
- \************************************/
274
- /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
275
-
276
-
277
- var __importDefault = (this && this.__importDefault) || function (mod) {
278
- return (mod && mod.__esModule) ? mod : { "default": mod };
279
- };
280
- Object.defineProperty(exports, "__esModule", ({ value: true }));
281
- exports.AbilityPolicyEffect = void 0;
282
- const AbilityCode_1 = __importDefault(__webpack_require__(/*! ./AbilityCode */ "./src/AbilityCode.ts"));
283
- class AbilityPolicyEffect extends AbilityCode_1.default {
284
- static deny = new AbilityPolicyEffect('deny');
285
- static permit = new AbilityPolicyEffect('permit');
286
- }
287
- exports.AbilityPolicyEffect = AbilityPolicyEffect;
288
- exports["default"] = AbilityPolicyEffect;
289
-
290
-
291
- /***/ }),
292
-
293
- /***/ "./src/AbilityResolver.ts":
294
- /*!********************************!*\
295
- !*** ./src/AbilityResolver.ts ***!
296
- \********************************/
297
- /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
298
-
299
-
300
- var __importDefault = (this && this.__importDefault) || function (mod) {
301
- return (mod && mod.__esModule) ? mod : { "default": mod };
302
- };
303
- Object.defineProperty(exports, "__esModule", ({ value: true }));
304
- exports.AbilityResolver = void 0;
305
- const AbilityPolicyEffect_1 = __importDefault(__webpack_require__(/*! ./AbilityPolicyEffect */ "./src/AbilityPolicyEffect.ts"));
306
- const AbilityMatch_1 = __importDefault(__webpack_require__(/*! ./AbilityMatch */ "./src/AbilityMatch.ts"));
307
- const AbilityError_1 = __webpack_require__(/*! ./AbilityError */ "./src/AbilityError.ts");
308
- class AbilityResolver {
309
- policies;
310
- constructor(policyOrListOfPolicies) {
311
- this.policies = Array.isArray(policyOrListOfPolicies)
312
- ? policyOrListOfPolicies
313
- : [policyOrListOfPolicies];
314
- }
315
- /**
316
- * Resolve policy for the resource and action
317
- *
318
- @param action - Action
319
- * @param resource - Resource
320
- */
321
- resolve(action, resource) {
322
- const filteredPolicies = this.policies.filter(policy => {
323
- return AbilityResolver.isInActionContain(policy.action, String(action));
324
- });
325
- filteredPolicies.map(policy => policy.check(resource));
326
- this.policies = filteredPolicies;
327
- return this;
328
- }
329
- enforce(action, resource) {
330
- const resolver = this.resolve(action, resource);
331
- if (resolver) {
332
- if (resolver.isDeny()) {
333
- throw new AbilityError_1.PermissionError(resolver.getPolicy()?.name?.toString() || 'Unknown permission error');
334
- }
335
- }
336
- }
337
- /**
338
- * Get the last effect of the policy
339
- *
340
- * @returns {AbilityPolicyEffect | null}
341
- */
342
- getEffect() {
343
- const effects = this.policies.reduce((collect, policy, _index) => {
344
- if (policy.matchState.isEqual(AbilityMatch_1.default.match)) {
345
- return collect.concat(policy.effect);
346
- }
347
- return collect;
348
- }, []);
349
- if (effects.length) {
350
- return effects[effects.length - 1];
351
- }
352
- return null;
353
- }
354
- isPermit() {
355
- const effect = this.getEffect();
356
- return effect !== null && effect.isEqual(AbilityPolicyEffect_1.default.permit);
357
- }
358
- isDeny() {
359
- const effect = this.getEffect();
360
- return effect !== null && effect.isEqual(AbilityPolicyEffect_1.default.deny);
361
- }
362
- getPolicy() {
363
- const lastPolicy = this.policies.length ? this.policies[this.policies.length - 1] : null;
364
- return lastPolicy && lastPolicy.matchState.isEqual(AbilityMatch_1.default.match) ? lastPolicy : null;
365
- }
366
- /**
367
- * Check if the action is contained in another action
368
- * @param actionA - The first action to check
369
- * @param actionB - The second action to check
370
- */
371
- static isInActionContain(actionA, actionB) {
372
- const actionAArray = String(actionA).split('.');
373
- const actionBArray = String(actionB).split('.');
374
- const a = actionAArray.length >= actionBArray.length ? actionAArray : actionBArray;
375
- const b = actionBArray.length >= actionAArray.length ? actionBArray : actionAArray;
376
- return a
377
- .reduce((acc, chunk, index) => {
378
- const iterationRes = chunk === b[index] || b[index] === '*' || chunk === '*';
379
- return acc.concat(iterationRes);
380
- }, [])
381
- .every(Boolean);
382
- }
383
- }
384
- exports.AbilityResolver = AbilityResolver;
385
- exports["default"] = AbilityResolver;
386
-
387
-
388
135
  /***/ }),
389
136
 
390
137
  /***/ "./src/AbilityRule.ts":
@@ -420,7 +167,7 @@ class AbilityRule {
420
167
  this.name = name;
421
168
  this.subject = subject;
422
169
  this.resource = resource;
423
- this.condition = new AbilityCondition_1.default(condition);
170
+ this.condition = condition;
424
171
  }
425
172
  /**
426
173
  * Check if the rule is matched
@@ -429,25 +176,25 @@ class AbilityRule {
429
176
  check(resource) {
430
177
  let is = false;
431
178
  const [valueS, valueO] = this.extractValues(resource);
432
- if (AbilityCondition_1.default.LESS_THAN.isEqual(this.condition)) {
179
+ if (AbilityCondition_1.default.less_than.isEqual(this.condition)) {
433
180
  is = Number(valueS) < Number(valueO);
434
181
  }
435
- if (AbilityCondition_1.default.LESS_OR_EQUAL.isEqual(this.condition)) {
182
+ if (AbilityCondition_1.default.less_or_equal.isEqual(this.condition)) {
436
183
  is = Number(valueS) <= Number(valueO);
437
184
  }
438
- if (AbilityCondition_1.default.MORE_THAN.isEqual(this.condition)) {
185
+ if (AbilityCondition_1.default.more_than.isEqual(this.condition)) {
439
186
  is = Number(valueS) > Number(valueO);
440
187
  }
441
- if (AbilityCondition_1.default.MORE_OR_EQUAL.isEqual(this.condition)) {
188
+ if (AbilityCondition_1.default.more_or_equal.isEqual(this.condition)) {
442
189
  is = Number(valueS) >= Number(valueO);
443
190
  }
444
- if (AbilityCondition_1.default.EQUAL.isEqual(this.condition)) {
191
+ if (AbilityCondition_1.default.equal.isEqual(this.condition)) {
445
192
  is = valueS === valueO;
446
193
  }
447
- if (AbilityCondition_1.default.NOT_EQUAL.isEqual(this.condition)) {
194
+ if (AbilityCondition_1.default.not_equal.isEqual(this.condition)) {
448
195
  is = valueS !== valueO;
449
196
  }
450
- if (AbilityCondition_1.default.IN.isEqual(this.condition)) {
197
+ if (AbilityCondition_1.default.in.isEqual(this.condition)) {
451
198
  // [<some>] and [<some>]
452
199
  if (Array.isArray(valueS) && Array.isArray(valueO)) {
453
200
  is = valueS.some(v => valueO.find(v1 => v1 === v));
@@ -461,7 +208,7 @@ class AbilityRule {
461
208
  is = valueS.includes(valueO);
462
209
  }
463
210
  }
464
- if (AbilityCondition_1.default.NOT_IN.isEqual(this.condition)) {
211
+ if (AbilityCondition_1.default.not_in.isEqual(this.condition)) {
465
212
  // [<some>] and [<some>]
466
213
  if (Array.isArray(valueS) && Array.isArray(valueO)) {
467
214
  is = !valueS.some(v => valueO.find(v1 => v1 === v));
@@ -540,7 +287,7 @@ class AbilityRule {
540
287
  name,
541
288
  subject,
542
289
  resource,
543
- condition,
290
+ condition: new AbilityCondition_1.default(condition),
544
291
  });
545
292
  }
546
293
  /**
@@ -560,110 +307,6 @@ exports.AbilityRule = AbilityRule;
560
307
  exports["default"] = AbilityRule;
561
308
 
562
309
 
563
- /***/ }),
564
-
565
- /***/ "./src/AbilityRuleSet.ts":
566
- /*!*******************************!*\
567
- !*** ./src/AbilityRuleSet.ts ***!
568
- \*******************************/
569
- /***/ (function(__unused_webpack_module, exports, __webpack_require__) {
570
-
571
-
572
- var __importDefault = (this && this.__importDefault) || function (mod) {
573
- return (mod && mod.__esModule) ? mod : { "default": mod };
574
- };
575
- Object.defineProperty(exports, "__esModule", ({ value: true }));
576
- exports.AbilityRuleSet = void 0;
577
- const AbilityRule_1 = __importDefault(__webpack_require__(/*! ./AbilityRule */ "./src/AbilityRule.ts"));
578
- const AbilityCompare_1 = __importDefault(__webpack_require__(/*! ./AbilityCompare */ "./src/AbilityCompare.ts"));
579
- const AbilityMatch_1 = __importDefault(__webpack_require__(/*! ./AbilityMatch */ "./src/AbilityMatch.ts"));
580
- class AbilityRuleSet {
581
- state = AbilityMatch_1.default.pending;
582
- /**
583
- * List of rules
584
- */
585
- rules = [];
586
- /**
587
- * Rules compare method.\
588
- * For the «and» method the rule will be permitted if all\
589
- * rules will be returns «permit» status and for the «or» - if\
590
- * one of the rules returns as «permit»
591
- */
592
- compareMethod = AbilityCompare_1.default.and;
593
- /**
594
- * Group name
595
- */
596
- name;
597
- /**
598
- * Group ID
599
- */
600
- id;
601
- constructor(params) {
602
- const { name, id, compareMethod } = params;
603
- this.name = name;
604
- this.id = id;
605
- this.compareMethod = AbilityCompare_1.default.fromLiteral(compareMethod);
606
- // this.compareMethod = new AbilityCompare(compareMethod);
607
- }
608
- addRule(rule, compareMethod) {
609
- this.rules.push(rule);
610
- this.compareMethod = compareMethod;
611
- return this;
612
- }
613
- addRules(rules, compareMethod) {
614
- rules.forEach(rule => this.addRule(rule, compareMethod));
615
- return this;
616
- }
617
- check(resources) {
618
- this.state = AbilityMatch_1.default.mismatch;
619
- if (!this.rules.length) {
620
- return this.state;
621
- }
622
- const ruleCheckStates = this.rules.reduce((collect, rule) => {
623
- return collect.concat(rule.check(resources));
624
- }, []);
625
- if (AbilityCompare_1.default.and.isEqual(this.compareMethod)) {
626
- if (ruleCheckStates.every(ruleState => AbilityMatch_1.default.match.isEqual(ruleState))) {
627
- this.state = AbilityMatch_1.default.match;
628
- }
629
- }
630
- if (AbilityCompare_1.default.or.isEqual(this.compareMethod)) {
631
- if (ruleCheckStates.some(ruleState => AbilityMatch_1.default.match.isEqual(ruleState))) {
632
- this.state = AbilityMatch_1.default.match;
633
- }
634
- }
635
- return this.state;
636
- }
637
- /**
638
- * Parse the config JSON format to Group class instance
639
- */
640
- static parse(config) {
641
- const { id, name, rules, compareMethod } = config;
642
- const ruleSet = new AbilityRuleSet({
643
- compareMethod,
644
- name,
645
- id,
646
- });
647
- // Adding rules if exists
648
- if (rules && rules.length > 0) {
649
- const abilityRules = rules.map(ruleConfig => AbilityRule_1.default.parse(ruleConfig));
650
- ruleSet.addRules(abilityRules, ruleSet.compareMethod);
651
- }
652
- return ruleSet;
653
- }
654
- export() {
655
- return {
656
- id: this.id.toString(),
657
- name: this.name.toString(),
658
- compareMethod: this.compareMethod.code.toString(),
659
- rules: this.rules.map(rule => rule.export()),
660
- };
661
- }
662
- }
663
- exports.AbilityRuleSet = AbilityRuleSet;
664
- exports["default"] = AbilityRuleSet;
665
-
666
-
667
310
  /***/ }),
668
311
 
669
312
  /***/ "./src/playground.ts":
@@ -678,8 +321,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
678
321
  };
679
322
  Object.defineProperty(exports, "__esModule", ({ value: true }));
680
323
  const node_http_1 = __importDefault(__webpack_require__(/*! node:http */ "node:http"));
681
- const AbilityPolicy_1 = __importDefault(__webpack_require__(/*! ./AbilityPolicy */ "./src/AbilityPolicy.ts"));
682
- const AbilityResolver_1 = __importDefault(__webpack_require__(/*! ~/AbilityResolver */ "./src/AbilityResolver.ts"));
324
+ const AbilityCondition_1 = __importDefault(__webpack_require__(/*! ~/AbilityCondition */ "./src/AbilityCondition.ts"));
325
+ const AbilityRule_1 = __importDefault(__webpack_require__(/*! ./AbilityRule */ "./src/AbilityRule.ts"));
326
+ const AbilityMatch_1 = __importDefault(__webpack_require__(/*! ./AbilityMatch */ "./src/AbilityMatch.ts"));
683
327
  const server = node_http_1.default.createServer();
684
328
  server.on('request', (_req, res) => {
685
329
  const config = [
@@ -704,7 +348,7 @@ server.on('request', (_req, res) => {
704
348
  },
705
349
  ],
706
350
  },
707
- ]
351
+ ],
708
352
  },
709
353
  {
710
354
  id: 'bb758c1b-1015-4894-ba25-d23156e063cf',
@@ -737,28 +381,27 @@ server.on('request', (_req, res) => {
737
381
  ],
738
382
  },
739
383
  ];
740
- const policies = config.map(cfg => AbilityPolicy_1.default.parse(cfg));
741
- const result = new AbilityResolver_1.default(policies).resolve('order.status', {
742
- account: {
743
- roles: ['user', 'couch'],
744
- },
745
- order: {
746
- status: 'не обработан',
747
- },
748
- feature: {
749
- status: 'отменен',
750
- // status: 'завершен'
751
- },
384
+ const rule = new AbilityRule_1.default({
385
+ id: '<rule-id>',
386
+ name: 'Пользователь является владельцем заказа',
387
+ condition: AbilityCondition_1.default.equal,
388
+ subject: 'user.id',
389
+ resource: 'order.owner',
390
+ });
391
+ const matchState = rule.check({
392
+ user: { id: '1' },
393
+ order: { owner: '1' },
752
394
  });
395
+ const is = matchState.isEqual(AbilityMatch_1.default.match); // true
753
396
  res.statusCode = 200;
754
397
  res.setHeader('content-type', 'application/json');
755
398
  res.write(JSON.stringify({
756
- status: result.isDeny() ? 'deny' : 'permit',
399
+ status: 'ok',
757
400
  }));
758
401
  res.end();
759
402
  });
760
- server.listen(8080, 'localhost', () => {
761
- console.debug('server started at http://localhost:8080');
403
+ server.listen(8081, 'localhost', () => {
404
+ console.debug('server started at http://localhost:8081');
762
405
  });
763
406
 
764
407