follow-redirects 1.14.8 → 1.15.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of follow-redirects might be problematic. Click here for more details.

Files changed (3) hide show
  1. package/README.md +8 -1
  2. package/index.js +252 -154
  3. package/package.json +2 -3
package/README.md CHANGED
@@ -63,10 +63,17 @@ const { http, https } = require('follow-redirects');
63
63
 
64
64
  const options = url.parse('http://bit.ly/900913');
65
65
  options.maxRedirects = 10;
66
- options.beforeRedirect = (options, { headers }) => {
66
+ options.beforeRedirect = (options, response, request) => {
67
67
  // Use this to adjust the request options upon redirecting,
68
68
  // to inspect the latest response headers,
69
69
  // or to cancel the request by throwing an error
70
+
71
+ // response.headers = the redirect response headers
72
+ // response.statusCode = the redirect response code (eg. 301, 307, etc.)
73
+
74
+ // request.url = the requested URL that resulted in a redirect
75
+ // request.headers = the headers in the request that resulted in a redirect
76
+ // request.method = the method of the request that resulted in a redirect
70
77
  if (options.hostname === "example.com") {
71
78
  options.auth = "user:password";
72
79
  }
package/index.js CHANGED
@@ -6,6 +6,29 @@ var Writable = require("stream").Writable;
6
6
  var assert = require("assert");
7
7
  var debug = require("./debug");
8
8
 
9
+ // Whether to use the native URL object or the legacy url module
10
+ var useNativeURL = false;
11
+ try {
12
+ assert(new URL());
13
+ }
14
+ catch (error) {
15
+ useNativeURL = error.code === "ERR_INVALID_URL";
16
+ }
17
+
18
+ // URL fields to preserve in copy operations
19
+ var preservedUrlFields = [
20
+ "auth",
21
+ "host",
22
+ "hostname",
23
+ "href",
24
+ "path",
25
+ "pathname",
26
+ "port",
27
+ "protocol",
28
+ "query",
29
+ "search",
30
+ ];
31
+
9
32
  // Create handlers that pass events from native requests
10
33
  var events = ["abort", "aborted", "connect", "error", "socket", "timeout"];
11
34
  var eventHandlers = Object.create(null);
@@ -16,13 +39,19 @@ events.forEach(function (event) {
16
39
  });
17
40
 
18
41
  // Error types with codes
42
+ var InvalidUrlError = createErrorType(
43
+ "ERR_INVALID_URL",
44
+ "Invalid URL",
45
+ TypeError
46
+ );
19
47
  var RedirectionError = createErrorType(
20
48
  "ERR_FR_REDIRECTION_FAILURE",
21
49
  "Redirected request failed"
22
50
  );
23
51
  var TooManyRedirectsError = createErrorType(
24
52
  "ERR_FR_TOO_MANY_REDIRECTS",
25
- "Maximum number of redirects exceeded"
53
+ "Maximum number of redirects exceeded",
54
+ RedirectionError
26
55
  );
27
56
  var MaxBodyLengthExceededError = createErrorType(
28
57
  "ERR_FR_MAX_BODY_LENGTH_EXCEEDED",
@@ -33,6 +62,9 @@ var WriteAfterEndError = createErrorType(
33
62
  "write after end"
34
63
  );
35
64
 
65
+ // istanbul ignore next
66
+ var destroy = Writable.prototype.destroy || noop;
67
+
36
68
  // An HTTP(S) request that can be redirected
37
69
  function RedirectableRequest(options, responseCallback) {
38
70
  // Initialize the request
@@ -54,7 +86,13 @@ function RedirectableRequest(options, responseCallback) {
54
86
  // React to responses of native requests
55
87
  var self = this;
56
88
  this._onNativeResponse = function (response) {
57
- self._processResponse(response);
89
+ try {
90
+ self._processResponse(response);
91
+ }
92
+ catch (cause) {
93
+ self.emit("error", cause instanceof RedirectionError ?
94
+ cause : new RedirectionError({ cause: cause }));
95
+ }
58
96
  };
59
97
 
60
98
  // Perform the first request
@@ -63,10 +101,17 @@ function RedirectableRequest(options, responseCallback) {
63
101
  RedirectableRequest.prototype = Object.create(Writable.prototype);
64
102
 
65
103
  RedirectableRequest.prototype.abort = function () {
66
- abortRequest(this._currentRequest);
104
+ destroyRequest(this._currentRequest);
105
+ this._currentRequest.abort();
67
106
  this.emit("abort");
68
107
  };
69
108
 
109
+ RedirectableRequest.prototype.destroy = function (error) {
110
+ destroyRequest(this._currentRequest, error);
111
+ destroy.call(this, error);
112
+ return this;
113
+ };
114
+
70
115
  // Writes buffered data to the current native request
71
116
  RedirectableRequest.prototype.write = function (data, encoding, callback) {
72
117
  // Writing is not allowed if end has been called
@@ -75,10 +120,10 @@ RedirectableRequest.prototype.write = function (data, encoding, callback) {
75
120
  }
76
121
 
77
122
  // Validate input and shift parameters if necessary
78
- if (!(typeof data === "string" || typeof data === "object" && ("length" in data))) {
123
+ if (!isString(data) && !isBuffer(data)) {
79
124
  throw new TypeError("data should be a string, Buffer or Uint8Array");
80
125
  }
81
- if (typeof encoding === "function") {
126
+ if (isFunction(encoding)) {
82
127
  callback = encoding;
83
128
  encoding = null;
84
129
  }
@@ -107,11 +152,11 @@ RedirectableRequest.prototype.write = function (data, encoding, callback) {
107
152
  // Ends the current native request
108
153
  RedirectableRequest.prototype.end = function (data, encoding, callback) {
109
154
  // Shift parameters if necessary
110
- if (typeof data === "function") {
155
+ if (isFunction(data)) {
111
156
  callback = data;
112
157
  data = encoding = null;
113
158
  }
114
- else if (typeof encoding === "function") {
159
+ else if (isFunction(encoding)) {
115
160
  callback = encoding;
116
161
  encoding = null;
117
162
  }
@@ -179,6 +224,7 @@ RedirectableRequest.prototype.setTimeout = function (msecs, callback) {
179
224
  self.removeListener("abort", clearTimer);
180
225
  self.removeListener("error", clearTimer);
181
226
  self.removeListener("response", clearTimer);
227
+ self.removeListener("close", clearTimer);
182
228
  if (callback) {
183
229
  self.removeListener("timeout", callback);
184
230
  }
@@ -205,6 +251,7 @@ RedirectableRequest.prototype.setTimeout = function (msecs, callback) {
205
251
  this.on("abort", clearTimer);
206
252
  this.on("error", clearTimer);
207
253
  this.on("response", clearTimer);
254
+ this.on("close", clearTimer);
208
255
 
209
256
  return this;
210
257
  };
@@ -263,32 +310,36 @@ RedirectableRequest.prototype._performRequest = function () {
263
310
  var protocol = this._options.protocol;
264
311
  var nativeProtocol = this._options.nativeProtocols[protocol];
265
312
  if (!nativeProtocol) {
266
- this.emit("error", new TypeError("Unsupported protocol " + protocol));
267
- return;
313
+ throw new TypeError("Unsupported protocol " + protocol);
268
314
  }
269
315
 
270
316
  // If specified, use the agent corresponding to the protocol
271
317
  // (HTTP and HTTPS use different types of agents)
272
318
  if (this._options.agents) {
273
- var scheme = protocol.substr(0, protocol.length - 1);
319
+ var scheme = protocol.slice(0, -1);
274
320
  this._options.agent = this._options.agents[scheme];
275
321
  }
276
322
 
277
- // Create the native request
323
+ // Create the native request and set up its event handlers
278
324
  var request = this._currentRequest =
279
325
  nativeProtocol.request(this._options, this._onNativeResponse);
280
- this._currentUrl = url.format(this._options);
281
-
282
- // Set up event handlers
283
326
  request._redirectable = this;
284
- for (var e = 0; e < events.length; e++) {
285
- request.on(events[e], eventHandlers[events[e]]);
327
+ for (var event of events) {
328
+ request.on(event, eventHandlers[event]);
286
329
  }
287
330
 
331
+ // RFC7230§5.3.1: When making a request directly to an origin server, […]
332
+ // a client MUST send only the absolute path […] as the request-target.
333
+ this._currentUrl = /^\//.test(this._options.path) ?
334
+ url.format(this._options) :
335
+ // When making a request to a proxy, […]
336
+ // a client MUST send the target URI in absolute-form […].
337
+ this._options.path;
338
+
288
339
  // End a redirected request
289
340
  // (The first request must be ended explicitly with RedirectableRequest#end)
290
341
  if (this._isRedirect) {
291
- // Write the request entity and end.
342
+ // Write the request entity and end
292
343
  var i = 0;
293
344
  var self = this;
294
345
  var buffers = this._requestBodyBuffers;
@@ -336,98 +387,99 @@ RedirectableRequest.prototype._processResponse = function (response) {
336
387
  // the user agent MAY automatically redirect its request to the URI
337
388
  // referenced by the Location field value,
338
389
  // even if the specific status code is not understood.
339
- var location = response.headers.location;
340
- if (location && this._options.followRedirects !== false &&
341
- statusCode >= 300 && statusCode < 400) {
342
- // Abort the current request
343
- abortRequest(this._currentRequest);
344
- // Discard the remainder of the response to avoid waiting for data
345
- response.destroy();
346
-
347
- // RFC7231§6.4: A client SHOULD detect and intervene
348
- // in cyclical redirections (i.e., "infinite" redirection loops).
349
- if (++this._redirectCount > this._options.maxRedirects) {
350
- this.emit("error", new TooManyRedirectsError());
351
- return;
352
- }
353
-
354
- // RFC7231§6.4: Automatic redirection needs to done with
355
- // care for methods not known to be safe, […]
356
- // RFC7231§6.4.2–3: For historical reasons, a user agent MAY change
357
- // the request method from POST to GET for the subsequent request.
358
- if ((statusCode === 301 || statusCode === 302) && this._options.method === "POST" ||
359
- // RFC7231§6.4.4: The 303 (See Other) status code indicates that
360
- // the server is redirecting the user agent to a different resource […]
361
- // A user agent can perform a retrieval request targeting that URI
362
- // (a GET or HEAD request if using HTTP) […]
363
- (statusCode === 303) && !/^(?:GET|HEAD)$/.test(this._options.method)) {
364
- this._options.method = "GET";
365
- // Drop a possible entity and headers related to it
366
- this._requestBodyBuffers = [];
367
- removeMatchingHeaders(/^content-/i, this._options.headers);
368
- }
369
390
 
370
- // Drop the Host header, as the redirect might lead to a different host
371
- var currentHostHeader = removeMatchingHeaders(/^host$/i, this._options.headers);
391
+ // If the response is not a redirect; return it as-is
392
+ var location = response.headers.location;
393
+ if (!location || this._options.followRedirects === false ||
394
+ statusCode < 300 || statusCode >= 400) {
395
+ response.responseUrl = this._currentUrl;
396
+ response.redirects = this._redirects;
397
+ this.emit("response", response);
372
398
 
373
- // If the redirect is relative, carry over the host of the last request
374
- var currentUrlParts = url.parse(this._currentUrl);
375
- var currentHost = currentHostHeader || currentUrlParts.host;
376
- var currentUrl = /^\w+:/.test(location) ? this._currentUrl :
377
- url.format(Object.assign(currentUrlParts, { host: currentHost }));
399
+ // Clean up
400
+ this._requestBodyBuffers = [];
401
+ return;
402
+ }
378
403
 
379
- // Determine the URL of the redirection
380
- var redirectUrl;
381
- try {
382
- redirectUrl = url.resolve(currentUrl, location);
383
- }
384
- catch (cause) {
385
- this.emit("error", new RedirectionError(cause));
386
- return;
387
- }
404
+ // The response is a redirect, so abort the current request
405
+ destroyRequest(this._currentRequest);
406
+ // Discard the remainder of the response to avoid waiting for data
407
+ response.destroy();
408
+
409
+ // RFC7231§6.4: A client SHOULD detect and intervene
410
+ // in cyclical redirections (i.e., "infinite" redirection loops).
411
+ if (++this._redirectCount > this._options.maxRedirects) {
412
+ throw new TooManyRedirectsError();
413
+ }
414
+
415
+ // Store the request headers if applicable
416
+ var requestHeaders;
417
+ var beforeRedirect = this._options.beforeRedirect;
418
+ if (beforeRedirect) {
419
+ requestHeaders = Object.assign({
420
+ // The Host header was set by nativeProtocol.request
421
+ Host: response.req.getHeader("host"),
422
+ }, this._options.headers);
423
+ }
424
+
425
+ // RFC7231§6.4: Automatic redirection needs to done with
426
+ // care for methods not known to be safe, […]
427
+ // RFC7231§6.4.2–3: For historical reasons, a user agent MAY change
428
+ // the request method from POST to GET for the subsequent request.
429
+ var method = this._options.method;
430
+ if ((statusCode === 301 || statusCode === 302) && this._options.method === "POST" ||
431
+ // RFC7231§6.4.4: The 303 (See Other) status code indicates that
432
+ // the server is redirecting the user agent to a different resource […]
433
+ // A user agent can perform a retrieval request targeting that URI
434
+ // (a GET or HEAD request if using HTTP) […]
435
+ (statusCode === 303) && !/^(?:GET|HEAD)$/.test(this._options.method)) {
436
+ this._options.method = "GET";
437
+ // Drop a possible entity and headers related to it
438
+ this._requestBodyBuffers = [];
439
+ removeMatchingHeaders(/^content-/i, this._options.headers);
440
+ }
388
441
 
389
- // Create the redirected request
390
- debug("redirecting to", redirectUrl);
391
- this._isRedirect = true;
392
- var redirectUrlParts = url.parse(redirectUrl);
393
- Object.assign(this._options, redirectUrlParts);
442
+ // Drop the Host header, as the redirect might lead to a different host
443
+ var currentHostHeader = removeMatchingHeaders(/^host$/i, this._options.headers);
394
444
 
395
- // Drop confidential headers when redirecting to another scheme:domain
396
- if (redirectUrlParts.protocol !== currentUrlParts.protocol ||
397
- !isSameOrSubdomain(redirectUrlParts.host, currentHost)) {
398
- removeMatchingHeaders(/^(?:authorization|cookie)$/i, this._options.headers);
399
- }
445
+ // If the redirect is relative, carry over the host of the last request
446
+ var currentUrlParts = parseUrl(this._currentUrl);
447
+ var currentHost = currentHostHeader || currentUrlParts.host;
448
+ var currentUrl = /^\w+:/.test(location) ? this._currentUrl :
449
+ url.format(Object.assign(currentUrlParts, { host: currentHost }));
400
450
 
401
- // Evaluate the beforeRedirect callback
402
- if (typeof this._options.beforeRedirect === "function") {
403
- var responseDetails = { headers: response.headers };
404
- try {
405
- this._options.beforeRedirect.call(null, this._options, responseDetails);
406
- }
407
- catch (err) {
408
- this.emit("error", err);
409
- return;
410
- }
411
- this._sanitizeOptions(this._options);
412
- }
451
+ // Create the redirected request
452
+ var redirectUrl = resolveUrl(location, currentUrl);
453
+ debug("redirecting to", redirectUrl.href);
454
+ this._isRedirect = true;
455
+ spreadUrlObject(redirectUrl, this._options);
413
456
 
414
- // Perform the redirected request
415
- try {
416
- this._performRequest();
417
- }
418
- catch (cause) {
419
- this.emit("error", new RedirectionError(cause));
420
- }
457
+ // Drop confidential headers when redirecting to a less secure protocol
458
+ // or to a different domain that is not a superdomain
459
+ if (redirectUrl.protocol !== currentUrlParts.protocol &&
460
+ redirectUrl.protocol !== "https:" ||
461
+ redirectUrl.host !== currentHost &&
462
+ !isSubdomain(redirectUrl.host, currentHost)) {
463
+ removeMatchingHeaders(/^(?:authorization|cookie)$/i, this._options.headers);
421
464
  }
422
- else {
423
- // The response is not a redirect; return it as-is
424
- response.responseUrl = this._currentUrl;
425
- response.redirects = this._redirects;
426
- this.emit("response", response);
427
465
 
428
- // Clean up
429
- this._requestBodyBuffers = [];
466
+ // Evaluate the beforeRedirect callback
467
+ if (isFunction(beforeRedirect)) {
468
+ var responseDetails = {
469
+ headers: response.headers,
470
+ statusCode: statusCode,
471
+ };
472
+ var requestDetails = {
473
+ url: currentUrl,
474
+ method: method,
475
+ headers: requestHeaders,
476
+ };
477
+ beforeRedirect(this._options, responseDetails, requestDetails);
478
+ this._sanitizeOptions(this._options);
430
479
  }
480
+
481
+ // Perform the redirected request
482
+ this._performRequest();
431
483
  };
432
484
 
433
485
  // Wraps the key/value object of protocols with redirect functionality
@@ -447,26 +499,19 @@ function wrap(protocols) {
447
499
 
448
500
  // Executes a request, following redirects
449
501
  function request(input, options, callback) {
450
- // Parse parameters
451
- if (typeof input === "string") {
452
- var urlStr = input;
453
- try {
454
- input = urlToOptions(new URL(urlStr));
455
- }
456
- catch (err) {
457
- /* istanbul ignore next */
458
- input = url.parse(urlStr);
459
- }
502
+ // Parse parameters, ensuring that input is an object
503
+ if (isURL(input)) {
504
+ input = spreadUrlObject(input);
460
505
  }
461
- else if (URL && (input instanceof URL)) {
462
- input = urlToOptions(input);
506
+ else if (isString(input)) {
507
+ input = spreadUrlObject(parseUrl(input));
463
508
  }
464
509
  else {
465
510
  callback = options;
466
- options = input;
511
+ options = validateUrl(input);
467
512
  input = { protocol: protocol };
468
513
  }
469
- if (typeof options === "function") {
514
+ if (isFunction(options)) {
470
515
  callback = options;
471
516
  options = null;
472
517
  }
@@ -477,6 +522,9 @@ function wrap(protocols) {
477
522
  maxBodyLength: exports.maxBodyLength,
478
523
  }, input, options);
479
524
  options.nativeProtocols = nativeProtocols;
525
+ if (!isString(options.host) && !isString(options.hostname)) {
526
+ options.hostname = "::1";
527
+ }
480
528
 
481
529
  assert.equal(options.protocol, protocol, "protocol mismatch");
482
530
  debug("options", options);
@@ -499,27 +547,57 @@ function wrap(protocols) {
499
547
  return exports;
500
548
  }
501
549
 
502
- /* istanbul ignore next */
503
550
  function noop() { /* empty */ }
504
551
 
505
- // from https://github.com/nodejs/node/blob/master/lib/internal/url.js
506
- function urlToOptions(urlObject) {
507
- var options = {
508
- protocol: urlObject.protocol,
509
- hostname: urlObject.hostname.startsWith("[") ?
510
- /* istanbul ignore next */
511
- urlObject.hostname.slice(1, -1) :
512
- urlObject.hostname,
513
- hash: urlObject.hash,
514
- search: urlObject.search,
515
- pathname: urlObject.pathname,
516
- path: urlObject.pathname + urlObject.search,
517
- href: urlObject.href,
518
- };
519
- if (urlObject.port !== "") {
520
- options.port = Number(urlObject.port);
552
+ function parseUrl(input) {
553
+ var parsed;
554
+ /* istanbul ignore else */
555
+ if (useNativeURL) {
556
+ parsed = new URL(input);
557
+ }
558
+ else {
559
+ // Ensure the URL is valid and absolute
560
+ parsed = validateUrl(url.parse(input));
561
+ if (!isString(parsed.protocol)) {
562
+ throw new InvalidUrlError({ input });
563
+ }
564
+ }
565
+ return parsed;
566
+ }
567
+
568
+ function resolveUrl(relative, base) {
569
+ /* istanbul ignore next */
570
+ return useNativeURL ? new URL(relative, base) : parseUrl(url.resolve(base, relative));
571
+ }
572
+
573
+ function validateUrl(input) {
574
+ if (/^\[/.test(input.hostname) && !/^\[[:0-9a-f]+\]$/i.test(input.hostname)) {
575
+ throw new InvalidUrlError({ input: input.href || input });
576
+ }
577
+ if (/^\[/.test(input.host) && !/^\[[:0-9a-f]+\](:\d+)?$/i.test(input.host)) {
578
+ throw new InvalidUrlError({ input: input.href || input });
521
579
  }
522
- return options;
580
+ return input;
581
+ }
582
+
583
+ function spreadUrlObject(urlObject, target) {
584
+ var spread = target || {};
585
+ for (var key of preservedUrlFields) {
586
+ spread[key] = urlObject[key];
587
+ }
588
+
589
+ // Fix IPv6 hostname
590
+ if (spread.hostname.startsWith("[")) {
591
+ spread.hostname = spread.hostname.slice(1, -1);
592
+ }
593
+ // Ensure port is a number
594
+ if (spread.port !== "") {
595
+ spread.port = Number(spread.port);
596
+ }
597
+ // Concatenate path
598
+ spread.path = spread.search ? spread.pathname + spread.search : spread.pathname;
599
+
600
+ return spread;
523
601
  }
524
602
 
525
603
  function removeMatchingHeaders(regex, headers) {
@@ -534,40 +612,60 @@ function removeMatchingHeaders(regex, headers) {
534
612
  undefined : String(lastValue).trim();
535
613
  }
536
614
 
537
- function createErrorType(code, defaultMessage) {
538
- function CustomError(cause) {
615
+ function createErrorType(code, message, baseClass) {
616
+ // Create constructor
617
+ function CustomError(properties) {
539
618
  Error.captureStackTrace(this, this.constructor);
540
- if (!cause) {
541
- this.message = defaultMessage;
542
- }
543
- else {
544
- this.message = defaultMessage + ": " + cause.message;
545
- this.cause = cause;
546
- }
547
- }
548
- CustomError.prototype = new Error();
549
- CustomError.prototype.constructor = CustomError;
550
- CustomError.prototype.name = "Error [" + code + "]";
551
- CustomError.prototype.code = code;
619
+ Object.assign(this, properties || {});
620
+ this.code = code;
621
+ this.message = this.cause ? message + ": " + this.cause.message : message;
622
+ }
623
+
624
+ // Attach constructor and set default properties
625
+ CustomError.prototype = new (baseClass || Error)();
626
+ Object.defineProperties(CustomError.prototype, {
627
+ constructor: {
628
+ value: CustomError,
629
+ enumerable: false,
630
+ },
631
+ name: {
632
+ value: "Error [" + code + "]",
633
+ enumerable: false,
634
+ },
635
+ });
552
636
  return CustomError;
553
637
  }
554
638
 
555
- function abortRequest(request) {
556
- for (var e = 0; e < events.length; e++) {
557
- request.removeListener(events[e], eventHandlers[events[e]]);
639
+ function destroyRequest(request, error) {
640
+ for (var event of events) {
641
+ request.removeListener(event, eventHandlers[event]);
558
642
  }
559
643
  request.on("error", noop);
560
- request.abort();
644
+ request.destroy(error);
561
645
  }
562
646
 
563
- function isSameOrSubdomain(subdomain, domain) {
564
- if (subdomain === domain) {
565
- return true;
566
- }
567
- const dot = subdomain.length - domain.length - 1;
647
+ function isSubdomain(subdomain, domain) {
648
+ assert(isString(subdomain) && isString(domain));
649
+ var dot = subdomain.length - domain.length - 1;
568
650
  return dot > 0 && subdomain[dot] === "." && subdomain.endsWith(domain);
569
651
  }
570
652
 
653
+ function isString(value) {
654
+ return typeof value === "string" || value instanceof String;
655
+ }
656
+
657
+ function isFunction(value) {
658
+ return typeof value === "function";
659
+ }
660
+
661
+ function isBuffer(value) {
662
+ return typeof value === "object" && ("length" in value);
663
+ }
664
+
665
+ function isURL(value) {
666
+ return URL && value instanceof URL;
667
+ }
668
+
571
669
  // Exports
572
670
  module.exports = wrap({ http: http, https: https });
573
671
  module.exports.wrap = wrap;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "follow-redirects",
3
- "version": "1.14.8",
3
+ "version": "1.15.4",
4
4
  "description": "HTTP and HTTPS modules that follow redirects.",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
@@ -11,9 +11,8 @@
11
11
  "node": ">=4.0"
12
12
  },
13
13
  "scripts": {
14
- "test": "npm run lint && npm run mocha",
15
14
  "lint": "eslint *.js test",
16
- "mocha": "nyc mocha"
15
+ "test": "nyc mocha"
17
16
  },
18
17
  "repository": {
19
18
  "type": "git",