switch from axios to got package for client requests
authorJustin Wind <justin.wind+git@gmail.com>
Sun, 4 Jun 2023 19:06:11 +0000 (12:06 -0700)
committerJustin Wind <justin.wind+git@gmail.com>
Sun, 4 Jun 2023 19:06:11 +0000 (12:06 -0700)
lib/common.js
lib/communication.js
package-lock.json
package.json
test/lib/common.js
test/lib/communication.js

index 3bf7e55cc7e85a4cd050fd2708dc2726fa9f9d94..c1b8703d58d435a127a80fcfaec70b73358a2a2c 100644 (file)
@@ -17,20 +17,28 @@ const fileScope = (filename) => {
 
 
 /**
- * Pick out useful axios response fields.
- * @param {AxiosResponse} res
+ * Pick out useful got response fields.
+ * @param {GotResponse} res
  * @returns {Object}
  */
-const axiosResponseLogData = (res) => {
+const gotResponseLogData = (res) => {
   const data = pick(res, [
-    'status',
-    'statusText',
+    'statusCode',
+    'statusMessage',
     'headers',
-    'elapsedTimeMs',
-    'data',
+    'body',
   ]);
-  if (data.data) {
-    data.data = logTruncate(data.data, 100);
+  if (typeof res.body === 'string') {
+    data.body = logTruncate(data.body, 100);
+  } else if (res.body instanceof Buffer) {
+    data.body = `<Buffer ${res.body.byteLength} bytes>`;
+  }
+  data.elapsedTimeMs = res?.timings?.phases?.total;
+  if (res?.redirectUrls?.length) {
+    data.redirectUrls = res.redirectUrls;
+  }
+  if (res?.retryCount) {
+    data.retryCount = res.retryCount;
   }
   return data;
 };
@@ -98,23 +106,11 @@ const properURLComponentName = (component) => {
 }
 
 
-/**
- * Encodes single-level object as form data string.
- * @param {Object} data
- */
-const formData = (data) => {
-  const formData = new URLSearchParams();
-  Object.entries(data).forEach(([name, value]) => formData.set(name, value));
-  return formData.toString();
-};
-
-
 module.exports = {
   fileScope,
-  axiosResponseLogData,
+  gotResponseLogData,
   logTruncate,
   pick,
   setSymmetricDifference,
   properURLComponentName,
-  formData,
 };
\ No newline at end of file
index ce2bc4e25b8bc05b281b86f970cd1e152b30c35c..18a6e588070e39bb49790433885189703a3bc5d0 100644 (file)
@@ -1,11 +1,9 @@
 'use strict';
 
-const axios = require('axios');
 const { mf2 } = require('microformats-parser');
 const { parse: parseLinkHeader } = require('@squeep/web-linking');
 const { Iconv } = require('iconv');
 const { version: packageVersion, name: packageName } = require('../package.json');
-const { performance } = require('perf_hooks');
 const { randomBytes, createHash } = require('crypto');
 const { promisify } = require('util');
 const randomBytesAsync = promisify(randomBytes);
@@ -26,6 +24,7 @@ class Communication {
   /**
    * @param {Console} logger
    * @param {Object} options
+   * @param {Number=} options.timeout
    * @param {Object=} options.userAgent
    * @param {String=} options.userAgent.product
    * @param {String=} options.userAgent.version
@@ -34,20 +33,52 @@ class Communication {
   constructor(logger, options = {}) {
     this.logger = logger;
     this.options = options;
-    this.axios = axios.create({
-      headers: {
-        [Enum.Header.UserAgent]: Communication._userAgentString(options.userAgent),
-        [Enum.Header.Accept]: 'text/html, text/*;q=0.9, application/xhtml+xml;q=0.8, application/xml;q=0.8, */*;q=0.1',
-      },
-    });
-    this.axios.interceptors.request.use((request) => {
-      request.startTimestampMs = performance.now();
-      return request;
-    });
-    this.axios.interceptors.response.use((response) => {
-      response.elapsedTimeMs = performance.now() - response.config.startTimestampMs;
-      return response;
-    });
+
+    this._defaultAccept = options?.defaultAccept || 'text/html, text/*;q=0.9, application/xhtml+xml;q=0.8, application/xml;q=0.8, */*;q=0.1';
+    this._jsonAccept = options?.jsonAccept || [Enum.ContentType.ApplicationJson, Enum.ContentType.Any + ';q=0.1'].join(', ');
+
+    this.Got = undefined;
+    this.got = this._init; // Do the dynamic import on first attempt to use client.
+  }
+
+
+  /**
+   * Do a little dance to support this ESM client.
+   */
+  async _init(...args) {
+    if (!this.Got) {
+      // For some reason eslint is confused about import being supported here.
+      // eslint-disable-next-line
+      this.Got = await import('got');
+      this.got = this.Got.got.extend({
+        headers: {
+          [Enum.Header.UserAgent]: Communication._userAgentString(this.options.userAgent),
+          [Enum.Header.Accept]: this._defaultAccept,
+        },
+        timeout: {
+          request: this.options.timeout || 120000,
+        },
+        hooks: {
+          beforeRetry: [
+            this._onRetry,
+          ],
+        },
+      });
+    }
+    if (args.length) {
+      return this.got(...args);
+    }
+  }
+
+
+  /**
+   * Take notes on transient retries.
+   * @param {*} error
+   * @param {*} retryCount
+   */
+  _onRetry(error, retryCount) {
+    const _scope = _fileScope('_onRetry');
+    this.logger.debug(_scope, 'retry', { retryCount, error });
   }
 
 
@@ -135,46 +166,6 @@ class Communication {
   }
 
 
-  /**
-   * Valid response statuses.
-   * Allow 401 as a workaround for one specific client which return such on
-   * its client identifier endpoint when not yet authenticated.
-   * @param {Number} status
-   * @returns {Boolean}
-   */
-  static _validateStatus(status) {
-    return (status >= 200 && status < 300) || status == 401;
-  }
-
-
-  /**
-   * A request config skeleton.
-   * @param {String} method
-   * @param {URL} urlObj
-   * @param {String=} body
-   * @param {Object=} params
-   * @param {Object=} headers
-   * @returns {Object}
-   */
-  static _axiosConfig(method, urlObj, body, params = {}, headers = {}) {
-    const config = {
-      method,
-      url: `${urlObj.origin}${urlObj.pathname}`,
-      params: urlObj.searchParams,
-      headers,
-      ...(body && { data: body }),
-      // Setting this does not appear to be enough to keep axios from parsing JSON response into object
-      responseType: 'text',
-      // So force the matter by eliding all response transformations
-      transformResponse: [ (res) => res ],
-
-      validateStatus: Communication._validateStatus,
-    };
-    Object.entries(params).map(([k, v]) => config.params.set(k, v));
-    return config;
-  }
-
-
   /**
    * Isolate the base of a url.
    * mf2 parser needs this so that relative links can be made absolute.
@@ -208,7 +199,7 @@ class Communication {
       mediaType: mediaType.toLowerCase() || defaultContentType,
       params: params.reduce((obj, param) => {
         const [field, value] = param.split('=');
-        const isQuoted = value && value.charAt(0) === '"' && value.charAt(value.length - 1) === '"';
+        const isQuoted = value?.startsWith('"') && value?.endsWith('"');
         obj[field.toLowerCase()] = isQuoted ? value.slice(1, value.length - 1) : value;
         return obj;
       }, {}),
@@ -284,27 +275,34 @@ class Communication {
     };
     let response;
     try {
-      const fetchMicroformatConfig = Communication._axiosConfig('GET', urlObj);
-      response = await this.axios(fetchMicroformatConfig);
+      const fetchMicroformatConfig = {
+        method: 'GET',
+        url: urlObj,
+        responseType: 'buffer',
+      };
+      response = await this.got(fetchMicroformatConfig);
     } catch (e) {
       this.logger.error(_scope, 'microformat request failed', { error: e, ...logInfoData });
       return;
     }
-    logInfoData.response = common.axiosResponseLogData(response);
+    logInfoData.response = common.gotResponseLogData(response);
 
     // Normalize to utf8.
-    let body = response.data;
+    let body;
     const contentType = Communication._parseContentType(response.headers[Enum.Header.ContentType.toLowerCase()]);
+    // If a charset was specified, and it's not utf8ish, attempt to transliterate it to utf8.
     const nonUTF8Charset = !/utf-*8/i.test(contentType.params.charset) && contentType.params.charset;
     if (nonUTF8Charset) {
       try {
         const iconv = new Iconv(nonUTF8Charset, 'utf-8//translit//ignore');
-        body = iconv.convert(body).toString('utf8');
+        body = iconv.convert(response.body).toString('utf8');
       } catch (e) {
         // istanbul ignore next
         this.logger.error(_scope, 'iconv conversion error', { error: e, ...logInfoData });
         // Try to carry on, maybe the encoding will work anyhow...
       }
+    } else {
+      body = response.body.toString('utf8');
     }
 
     let microformat = {};
@@ -340,24 +338,22 @@ class Communication {
     };
     let response;
     try {
-      const fetchJSONConfig = Communication._axiosConfig('GET', urlObj, undefined, undefined, {
-        [Enum.Header.Accept]: [Enum.ContentType.ApplicationJson, Enum.ContentType.Any + ';q=0.1'].join(', '),
-      });
-      response = await this.axios(fetchJSONConfig);
+      const fetchJSONConfig = {
+        method: 'GET',
+        url: urlObj,
+        headers: {
+          [Enum.Header.Accept]: this._jsonAccept,
+        },
+        responseType: 'json',
+      };
+      response = await this.got(fetchJSONConfig);
     } catch (e) {
       this.logger.error(_scope, 'json request failed', { error: e, ...logInfoData });
       return;
     }
-    logInfoData.response = common.axiosResponseLogData(response);
+    logInfoData.response = common.gotResponseLogData(response);
 
-    let data;
-    try {
-      data = JSON.parse(response.data);
-    } catch (e) {
-      this.logger.error(_scope, 'json parsing failed', { error: e, ...logInfoData });
-    }
-
-    return data;
+    return response.body;
   }
 
 
@@ -760,27 +756,25 @@ class Communication {
   async redeemCode(urlObj, code, codeVerifier, clientId, redirectURI) {
     const _scope = _fileScope('redeemCode');
 
-    const formData = common.formData({
-      'grant_type': 'authorization_code',
-      code,
-      'client_id': clientId,
-      'redirect_uri': redirectURI,
-      'code_verifier': codeVerifier,
-    });
-
-    const postRedeemCodeConfig = Communication._axiosConfig('POST', urlObj, formData, {}, {
-      [Enum.Header.ContentType]: Enum.ContentType.ApplicationForm,
-      [Enum.Header.Accept]: `${Enum.ContentType.ApplicationJson}, ${Enum.ContentType.Any};q=0.1`,
-    });
+    const postRedeemCodeConfig = {
+      url: urlObj,
+      method: 'POST',
+      headers: {
+        [Enum.Header.Accept]: this._jsonAccept,
+      },
+      form: {
+        'grant_type': 'authorization_code',
+        code,
+        'client_id': clientId,
+        'redirect_uri': redirectURI,
+        'code_verifier': codeVerifier,
+      },
+      responseType: 'json',
+    };
 
     try {
-      const response = await this.axios(postRedeemCodeConfig);
-      try {
-        return JSON.parse(response.data);
-      } catch (e) {
-        this.logger.error(_scope, 'failed to parse json', { error: e, response });
-        throw e;
-      }
+      const response = await this.got(postRedeemCodeConfig);
+      return response.body;
     } catch (e) {
       this.logger.error(_scope, 'redeem code request failed', { error: e, url: urlObj.href });
       return;
@@ -789,7 +783,7 @@ class Communication {
 
 
   /**
-   * Deprecated method name.
+   * Deprecated method name alias.
    * @see redeemCode
    * @param {URL} urlObj
    * @param {String} code
@@ -812,21 +806,24 @@ class Communication {
   async introspectToken(introspectionUrlObj, authorizationHeader, token) {
     const _scope = _fileScope('introspectToken');
 
-    const formData = common.formData({ token });
-    const postIntrospectConfig = Communication._axiosConfig('POST', introspectionUrlObj, formData, {}, {
-      [Enum.Header.Authorization]: authorizationHeader,
-      [Enum.Header.ContentType]: Enum.ContentType.ApplicationForm,
-      [Enum.Header.Accept]: `${Enum.ContentType.ApplicationJson}, ${Enum.ContentType.Any};q=0.1`,
-    });
-    delete postIntrospectConfig.validateStatus;  // only accept success
+    const postIntrospectConfig = {
+      url: introspectionUrlObj,
+      method: 'POST',
+      headers: {
+        [Enum.Header.Authorization]: authorizationHeader,
+        [Enum.Header.Accept]: this._jsonAccept,
+      },
+      form: {
+        token,
+      },
+      responseType: 'json',
+    };
 
-    let tokenInfo;
     try {
-      const response = await this.axios(postIntrospectConfig);
+      const response = await this.got(postIntrospectConfig);
       this.logger.debug(_scope, 'response', { response });
       // check status
       try {
-        tokenInfo = JSON.parse(response.data);
         const {
           active,
           me,
@@ -834,7 +831,11 @@ class Communication {
           scope,
           exp,
           iat,
-        } = tokenInfo;
+        } = response.body;
+
+        if (![true, false].includes(active)) {
+          throw new RangeError('missing required response field "active"');
+        }
 
         return {
           active,
@@ -845,7 +846,7 @@ class Communication {
           ...(iat && { iat: Number(iat) }),
         };
       } catch (e) {
-        this.logger.error(_scope, 'failed to parse json', { error: e, response });
+        this.logger.error(_scope, 'failed to parse json', { error: e, response: common.gotResponseLogData(response) });
         throw e;
       }
     } catch (e) {
@@ -868,15 +869,16 @@ class Communication {
     const _scope = _fileScope('deliverTicket');
 
     try {
-      const ticketPayload = {
-        ticket,
-        resource: resourceUrlObj.href,
-        subject: subjectUrlObj.href,
+      const ticketConfig = {
+        method: 'POST',
+        url: ticketEndpointUrlObj,
+        form: {
+          ticket,
+          resource: resourceUrlObj.href,
+          subject: subjectUrlObj.href,
+        },
       };
-      const ticketConfig = Communication._axiosConfig('POST', ticketEndpointUrlObj, ticketPayload, {}, {
-        [Enum.Header.ContentType]: Enum.ContentType.ApplicationForm,
-      });
-      return await this.axios(ticketConfig);
+      return await this.got(ticketConfig);
     } catch (e) {
       this.logger.error(_scope, 'ticket delivery request failed', { error: e, url: ticketEndpointUrlObj.href });
       throw e;
index 83913841cc0b898b9d5a037e5ae446c6140da42e..959766482cad9fbdc78ff82b1ffe7c1bd8436fff 100644 (file)
@@ -10,7 +10,7 @@
       "license": "ISC",
       "dependencies": {
         "@squeep/web-linking": "^1.0.7",
-        "axios": "^1.4.0",
+        "got": "^13.0.0",
         "iconv": "^3.0.1",
         "ip-address": "^8.1.0",
         "microformats-parser": "^1.4.1"
         "node": ">= 8"
       }
     },
+    "node_modules/@sindresorhus/is": {
+      "version": "5.4.0",
+      "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.4.0.tgz",
+      "integrity": "sha512-Ggh6E9AnMpiNXlbXfFUcWE9qm408rL8jDi7+PMBBx7TMbwEmiqAiSmZ+zydYwxcJLqPGNDoLc9mXDuMDBZg0sA==",
+      "engines": {
+        "node": ">=14.16"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/is?sponsor=1"
+      }
+    },
     "node_modules/@sinonjs/commons": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
         "node": ">=14.0"
       }
     },
+    "node_modules/@szmarczak/http-timer": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz",
+      "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==",
+      "dependencies": {
+        "defer-to-connect": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=14.16"
+      }
+    },
+    "node_modules/@types/http-cache-semantics": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
+      "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ=="
+    },
     "node_modules/acorn": {
       "version": "8.8.2",
       "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
       "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
       "dev": true
     },
-    "node_modules/asynckit": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
-      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
-    },
-    "node_modules/axios": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
-      "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
-      "dependencies": {
-        "follow-redirects": "^1.15.0",
-        "form-data": "^4.0.0",
-        "proxy-from-env": "^1.1.0"
-      }
-    },
     "node_modules/balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
       "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
       "dev": true
     },
+    "node_modules/cacheable-lookup": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz",
+      "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==",
+      "engines": {
+        "node": ">=14.16"
+      }
+    },
+    "node_modules/cacheable-request": {
+      "version": "10.2.10",
+      "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.10.tgz",
+      "integrity": "sha512-v6WB+Epm/qO4Hdlio/sfUn69r5Shgh39SsE9DSd4bIezP0mblOlObI+I0kUEM7J0JFc+I7pSeMeYaOYtX1N/VQ==",
+      "dependencies": {
+        "@types/http-cache-semantics": "^4.0.1",
+        "get-stream": "^6.0.1",
+        "http-cache-semantics": "^4.1.1",
+        "keyv": "^4.5.2",
+        "mimic-response": "^4.0.0",
+        "normalize-url": "^8.0.0",
+        "responselike": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=14.16"
+      }
+    },
     "node_modules/caching-transform": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz",
       "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
       "dev": true
     },
-    "node_modules/combined-stream": {
-      "version": "1.0.8",
-      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
-      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
-      "dependencies": {
-        "delayed-stream": "~1.0.0"
-      },
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
     "node_modules/commondir": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
         "node": ">=0.10.0"
       }
     },
+    "node_modules/decompress-response": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+      "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+      "dependencies": {
+        "mimic-response": "^3.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/decompress-response/node_modules/mimic-response": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+      "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/deep-is": {
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/delayed-stream": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
-      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+    "node_modules/defer-to-connect": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
+      "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==",
       "engines": {
-        "node": ">=0.4.0"
+        "node": ">=10"
       }
     },
     "node_modules/diff": {
       "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
       "dev": true
     },
-    "node_modules/follow-redirects": {
-      "version": "1.15.2",
-      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
-      "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
-      "funding": [
-        {
-          "type": "individual",
-          "url": "https://github.com/sponsors/RubenVerborgh"
-        }
-      ],
-      "engines": {
-        "node": ">=4.0"
-      },
-      "peerDependenciesMeta": {
-        "debug": {
-          "optional": true
-        }
-      }
-    },
     "node_modules/foreground-child": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz",
         "node": ">=8.0.0"
       }
     },
-    "node_modules/form-data": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
-      "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
-      "dependencies": {
-        "asynckit": "^0.4.0",
-        "combined-stream": "^1.0.8",
-        "mime-types": "^2.1.12"
-      },
+    "node_modules/form-data-encoder": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz",
+      "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==",
       "engines": {
-        "node": ">= 6"
+        "node": ">= 14.17"
       }
     },
     "node_modules/fromentries": {
         "node": ">=8.0.0"
       }
     },
+    "node_modules/get-stream": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+      "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/glob": {
       "version": "7.2.0",
       "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/got": {
+      "version": "13.0.0",
+      "resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz",
+      "integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==",
+      "dependencies": {
+        "@sindresorhus/is": "^5.2.0",
+        "@szmarczak/http-timer": "^5.0.1",
+        "cacheable-lookup": "^7.0.0",
+        "cacheable-request": "^10.2.8",
+        "decompress-response": "^6.0.0",
+        "form-data-encoder": "^2.1.2",
+        "get-stream": "^6.0.1",
+        "http2-wrapper": "^2.1.10",
+        "lowercase-keys": "^3.0.0",
+        "p-cancelable": "^3.0.0",
+        "responselike": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=16"
+      },
+      "funding": {
+        "url": "https://github.com/sindresorhus/got?sponsor=1"
+      }
+    },
     "node_modules/graceful-fs": {
       "version": "4.2.11",
       "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
       "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
       "dev": true
     },
+    "node_modules/http-cache-semantics": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
+      "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
+    },
+    "node_modules/http2-wrapper": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.0.tgz",
+      "integrity": "sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==",
+      "dependencies": {
+        "quick-lru": "^5.1.1",
+        "resolve-alpn": "^1.2.0"
+      },
+      "engines": {
+        "node": ">=10.19.0"
+      }
+    },
     "node_modules/iconv": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/iconv/-/iconv-3.0.1.tgz",
         "node": ">=4"
       }
     },
+    "node_modules/json-buffer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
+    },
     "node_modules/json-schema-traverse": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
       "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==",
       "dev": true
     },
+    "node_modules/keyv": {
+      "version": "4.5.2",
+      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz",
+      "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==",
+      "dependencies": {
+        "json-buffer": "3.0.1"
+      }
+    },
     "node_modules/levn": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/lowercase-keys": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
+      "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==",
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/lru-cache": {
       "version": "5.1.1",
       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
         "node": ">=10"
       }
     },
-    "node_modules/mime-db": {
-      "version": "1.52.0",
-      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
-      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+    "node_modules/mimic-response": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz",
+      "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==",
       "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/mime-types": {
-      "version": "2.1.35",
-      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
-      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
-      "dependencies": {
-        "mime-db": "1.52.0"
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
       },
-      "engines": {
-        "node": ">= 0.6"
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/minimatch": {
         "node": ">=0.10.0"
       }
     },
+    "node_modules/normalize-url": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz",
+      "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==",
+      "engines": {
+        "node": ">=14.16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/nyc": {
       "version": "15.1.0",
       "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz",
         "node": ">= 0.4.0"
       }
     },
+    "node_modules/p-cancelable": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz",
+      "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==",
+      "engines": {
+        "node": ">=12.20"
+      }
+    },
     "node_modules/p-limit": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
         "node": ">=8"
       }
     },
-    "node_modules/proxy-from-env": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
-      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
-    },
     "node_modules/pseudomap": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
         }
       ]
     },
+    "node_modules/quick-lru": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
+      "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/randombytes": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/resolve-alpn": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
+      "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="
+    },
     "node_modules/resolve-from": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
         "node": ">=4"
       }
     },
+    "node_modules/responselike": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz",
+      "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==",
+      "dependencies": {
+        "lowercase-keys": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=14.16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/reusify": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
         "fastq": "^1.6.0"
       }
     },
+    "@sindresorhus/is": {
+      "version": "5.4.0",
+      "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.4.0.tgz",
+      "integrity": "sha512-Ggh6E9AnMpiNXlbXfFUcWE9qm408rL8jDi7+PMBBx7TMbwEmiqAiSmZ+zydYwxcJLqPGNDoLc9mXDuMDBZg0sA=="
+    },
     "@sinonjs/commons": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
       "resolved": "https://registry.npmjs.org/@squeep/web-linking/-/web-linking-1.0.7.tgz",
       "integrity": "sha512-9d3QijrWc/WNE7p/K7NLUHbmPf92CURRqfzDLV0cGqYNA4QWAPfzwC8hxWpdUkUnep3KakvLKK60l0kEBMM3ag=="
     },
+    "@szmarczak/http-timer": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz",
+      "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==",
+      "requires": {
+        "defer-to-connect": "^2.0.1"
+      }
+    },
+    "@types/http-cache-semantics": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz",
+      "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ=="
+    },
     "acorn": {
       "version": "8.8.2",
       "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
       "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
       "dev": true
     },
-    "asynckit": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
-      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
-    },
-    "axios": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
-      "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
-      "requires": {
-        "follow-redirects": "^1.15.0",
-        "form-data": "^4.0.0",
-        "proxy-from-env": "^1.1.0"
-      }
-    },
     "balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
       "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
       "dev": true
     },
+    "cacheable-lookup": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz",
+      "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w=="
+    },
+    "cacheable-request": {
+      "version": "10.2.10",
+      "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.10.tgz",
+      "integrity": "sha512-v6WB+Epm/qO4Hdlio/sfUn69r5Shgh39SsE9DSd4bIezP0mblOlObI+I0kUEM7J0JFc+I7pSeMeYaOYtX1N/VQ==",
+      "requires": {
+        "@types/http-cache-semantics": "^4.0.1",
+        "get-stream": "^6.0.1",
+        "http-cache-semantics": "^4.1.1",
+        "keyv": "^4.5.2",
+        "mimic-response": "^4.0.0",
+        "normalize-url": "^8.0.0",
+        "responselike": "^3.0.0"
+      }
+    },
     "caching-transform": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz",
       "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
       "dev": true
     },
-    "combined-stream": {
-      "version": "1.0.8",
-      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
-      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
-      "requires": {
-        "delayed-stream": "~1.0.0"
-      }
-    },
     "commondir": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
       "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
       "dev": true
     },
+    "decompress-response": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+      "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+      "requires": {
+        "mimic-response": "^3.1.0"
+      },
+      "dependencies": {
+        "mimic-response": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+          "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="
+        }
+      }
+    },
     "deep-is": {
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
         "strip-bom": "^4.0.0"
       }
     },
-    "delayed-stream": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
-      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
+    "defer-to-connect": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz",
+      "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="
     },
     "diff": {
       "version": "5.0.0",
       "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
       "dev": true
     },
-    "follow-redirects": {
-      "version": "1.15.2",
-      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
-      "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
-    },
     "foreground-child": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz",
         "signal-exit": "^3.0.2"
       }
     },
-    "form-data": {
-      "version": "4.0.0",
-      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
-      "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
-      "requires": {
-        "asynckit": "^0.4.0",
-        "combined-stream": "^1.0.8",
-        "mime-types": "^2.1.12"
-      }
+    "form-data-encoder": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz",
+      "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw=="
     },
     "fromentries": {
       "version": "1.3.2",
       "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
       "dev": true
     },
+    "get-stream": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+      "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="
+    },
     "glob": {
       "version": "7.2.0",
       "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
         "type-fest": "^0.20.2"
       }
     },
+    "got": {
+      "version": "13.0.0",
+      "resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz",
+      "integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==",
+      "requires": {
+        "@sindresorhus/is": "^5.2.0",
+        "@szmarczak/http-timer": "^5.0.1",
+        "cacheable-lookup": "^7.0.0",
+        "cacheable-request": "^10.2.8",
+        "decompress-response": "^6.0.0",
+        "form-data-encoder": "^2.1.2",
+        "get-stream": "^6.0.1",
+        "http2-wrapper": "^2.1.10",
+        "lowercase-keys": "^3.0.0",
+        "p-cancelable": "^3.0.0",
+        "responselike": "^3.0.0"
+      }
+    },
     "graceful-fs": {
       "version": "4.2.11",
       "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
       "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
       "dev": true
     },
+    "http-cache-semantics": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
+      "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
+    },
+    "http2-wrapper": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.0.tgz",
+      "integrity": "sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==",
+      "requires": {
+        "quick-lru": "^5.1.1",
+        "resolve-alpn": "^1.2.0"
+      }
+    },
     "iconv": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/iconv/-/iconv-3.0.1.tgz",
       "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
       "dev": true
     },
+    "json-buffer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="
+    },
     "json-schema-traverse": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
       "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==",
       "dev": true
     },
+    "keyv": {
+      "version": "4.5.2",
+      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.2.tgz",
+      "integrity": "sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==",
+      "requires": {
+        "json-buffer": "3.0.1"
+      }
+    },
     "levn": {
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
         "is-unicode-supported": "^0.1.0"
       }
     },
+    "lowercase-keys": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
+      "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="
+    },
     "lru-cache": {
       "version": "5.1.1",
       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
         "parse5": "^6.0.0"
       }
     },
-    "mime-db": {
-      "version": "1.52.0",
-      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
-      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
-    },
-    "mime-types": {
-      "version": "2.1.35",
-      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
-      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
-      "requires": {
-        "mime-db": "1.52.0"
-      }
+    "mimic-response": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz",
+      "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg=="
     },
     "minimatch": {
       "version": "3.1.2",
       "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
       "dev": true
     },
+    "normalize-url": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz",
+      "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw=="
+    },
     "nyc": {
       "version": "15.1.0",
       "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz",
       "integrity": "sha512-jd0cvB8qQ5uVt0lvCIexBaROw1KyKm5sbulg2fWOHjETisuCzWyt+eTZKEMs8v6HwzoGs8xik26jg7eCM6pS+A==",
       "dev": true
     },
+    "p-cancelable": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz",
+      "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="
+    },
     "p-limit": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
         "fromentries": "^1.2.0"
       }
     },
-    "proxy-from-env": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
-      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
-    },
     "pseudomap": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
       "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
       "dev": true
     },
+    "quick-lru": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
+      "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="
+    },
     "randombytes": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
         "supports-preserve-symlinks-flag": "^1.0.0"
       }
     },
+    "resolve-alpn": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
+      "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="
+    },
     "resolve-from": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
       "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
       "dev": true
     },
+    "responselike": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz",
+      "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==",
+      "requires": {
+        "lowercase-keys": "^3.0.0"
+      }
+    },
     "reusify": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
index 2a41497a15f9f4b133d835ff77a130a386148bce..e0df23cc50e4ca40b8103175103aea5df2242243 100644 (file)
@@ -26,7 +26,7 @@
   "license": "ISC",
   "dependencies": {
     "@squeep/web-linking": "^1.0.7",
-    "axios": "^1.4.0",
+    "got": "^13.0.0",
     "iconv": "^3.0.1",
     "ip-address": "^8.1.0",
     "microformats-parser": "^1.4.1"
index 20911c1d5d745eb0749df5dd69a58cbd173163f2..ff250a4c43098e0be305ab523d2d396eb6defc8b 100644 (file)
@@ -46,47 +46,121 @@ describe('common', function () {
     });
   }); // logTruncate
 
-  describe('axiosResponseLogData', function () {
+  describe('gotResponseLogData', function () {
     it('covers', function () {
       const response = {
-        status: 200,
-        statusText: 'OK',
+        statusCode: 200,
+        statusMessage: 'OK',
         headers: {
           'Content-Type': 'text/plain',
         },
         otherData: 'blah',
-        data: 'Old Mother West Wind had stopped to talk with the Slender Fir Tree. "I\'ve just come across the Green Meadows," said Old Mother West Wind, “and there I saw the Best Thing in the World.”',
+        timings: {
+          phases: {
+            total: 89,
+          },
+        },
+        retryCount: 0,
+        redirectUrls: [],
+        body: 'Old Mother West Wind had stopped to talk with the Slender Fir Tree. "I\'ve just come across the Green Meadows," said Old Mother West Wind, “and there I saw the Best Thing in the World.”',
+      };
+      const expected = {
+        statusCode: 200,
+        statusMessage: 'OK',
+        elapsedTimeMs: 89,
+        headers: {
+          'Content-Type': 'text/plain',
+        },
+        body: 'Old Mother West Wind had stopped to talk with the Slender Fir Tree. "I\'ve just come across the Green... (184 bytes)',
+      };
+      const result = common.gotResponseLogData(response);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('covers no body', function () {
+      const response = {
+        statusCode: 200,
+        statusMessage: 'OK',
+        headers: {
+          'Content-Type': 'text/plain',
+        },
+        timings: {
+          phases: {
+            total: 89,
+          },
+        },
+        retryCount: 1,
       };
       const expected = {
-        status: 200,
-        statusText: 'OK',
+        statusCode: 200,
+        statusMessage: 'OK',
         headers: {
           'Content-Type': 'text/plain',
         },
-        data: 'Old Mother West Wind had stopped to talk with the Slender Fir Tree. "I\'ve just come across the Green... (184 bytes)',
+        elapsedTimeMs: 89,
+        retryCount: 1,
+      };
+      const result = common.gotResponseLogData(response);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('covers json', function () {
+      const response = {
+        statusCode: 200,
+        statusMessage: 'OK',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        timings: {
+          phases: {
+            total: 89,
+          },
+        },
+        body: {
+          foo: 'bar',
+        },
+        redirectUrls: ['https://redirect.example.com/'],
+      };
+      const expected = {
+        statusCode: 200,
+        statusMessage: 'OK',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        elapsedTimeMs: 89,
+        body: {
+          foo: 'bar',
+        },
+        redirectUrls: ['https://redirect.example.com/'],
       };
-      const result = common.axiosResponseLogData(response);
+      const result = common.gotResponseLogData(response);
       assert.deepStrictEqual(result, expected);
     });
-    it('covers no data', function () {
+    it('covers buffer', function () {
       const response = {
-        status: 200,
-        statusText: 'OK',
+        statusCode: 200,
+        statusMessage: 'OK',
         headers: {
           'Content-Type': 'text/plain',
         },
+        timings: {
+          phases: {
+            total: 89,
+          },
+        },
+        body: Buffer.from('ᘛ⁐̤ᕐᐷ'),
       };
       const expected = {
-        status: 200,
-        statusText: 'OK',
+        statusCode: 200,
+        statusMessage: 'OK',
         headers: {
           'Content-Type': 'text/plain',
         },
+        elapsedTimeMs: 89,
+        body: '<Buffer 14 bytes>',
       };
-      const result = common.axiosResponseLogData(response);
+      const result = common.gotResponseLogData(response);
       assert.deepStrictEqual(result, expected);
     });
-  }); // axiosResponseLogData
+  }); // gotResponseLogData
 
   describe('setSymmetricDifference', function () {
     it('covers difference', function () {
@@ -120,14 +194,4 @@ describe('common', function () {
     });
   }); // properURLComponentName
 
-  describe('formData', function () {
-    it('covers', function () {
-      const result = common.formData({
-        key: 'value',
-        foo: 'bar',
-      });
-      assert.strictEqual(result, 'key=value&foo=bar');
-    });
-  }); // formData
-
 }); // common
\ No newline at end of file
index 7ecad8e817b396f5681953ec72e822bf65afe1e1..46ef89e2642ff206207fce8783052d1ea9fee2a5 100644 (file)
@@ -16,11 +16,12 @@ const testData = require('../test-data/communication');
 describe('Communication', function () {
   let communication, options;
 
-  beforeEach(function () {
+  beforeEach(async function () {
     options = {};
     communication = new Communication(stubLogger, options);
+    await communication._init();
     stubLogger._reset();
-    sinon.stub(communication, 'axios');
+    sinon.stub(communication, 'got');
   });
   afterEach(function () {
     sinon.restore();
@@ -34,20 +35,19 @@ describe('Communication', function () {
     communication = new Communication(stubLogger);
   });
 
-  describe('Axios timing coverage', function () {
-    const request = {};
-    const response = {
-      config: request,
-    };
-    it('tags request', function () {
-      communication.axios.interceptors.request.handlers[0].fulfilled(request);
-      assert(request.startTimestampMs);
+  describe('_init', function () {
+    it('covers first use', async function () {
+      await communication._init({});
+      assert(communication.got.called);
     });
-    it('tags response', function () {
-      communication.axios.interceptors.response.handlers[0].fulfilled(response);
-      assert(response.elapsedTimeMs);
+  }); // _init
+
+  describe('_onRetry', function () {
+    it('covers', function () {
+      communication._onRetry(new Error('oh no'), 1);
+      assert(communication.logger.debug.called);
     });
-  }); // Axios timing coverage
+  }); // _onRetry
 
   describe('_challengeFromVerifier', function () {
     it('covers', function () {
@@ -127,93 +127,6 @@ describe('Communication', function () {
     });
   }); // userAgentString
 
-  describe('Axios Configurations', function () {
-    let requestUrl, expectedUrl;
-    beforeEach(function () {
-      requestUrl = 'https://example.com/client_id';
-      expectedUrl = 'https://example.com/client_id';
-    });
-    it('_axiosConfig', function () {
-      const method = 'GET';
-      const contentType = 'text/plain';
-      const body = undefined;
-      const params = {
-        'extra_parameter': 'foobar',
-      };
-      const urlObj = new URL(requestUrl);
-      const expectedUrlObj = new URL(`${requestUrl}?extra_parameter=foobar`);
-      const expected = {
-        method,
-        url: 'https://example.com/client_id',
-        headers: {
-          'Content-Type': 'text/plain',
-        },
-        params: expectedUrlObj.searchParams,
-        responseType: 'text',
-        validateStatus: Communication._validateStatus,
-      };
-      const result = Communication._axiosConfig(method, urlObj, body, params, {
-        'Content-Type': contentType,
-      });
-      delete result.transformResponse;
-      assert.deepStrictEqual(result, expected);
-    });
-    it('_axiosConfig covers defaults', function () {
-      const method = 'OPTIONS';
-      const urlObj = new URL(requestUrl);
-      const expectedUrlObj = new URL(requestUrl);
-      const expected = {
-        method,
-        url: expectedUrl,
-        headers: {},
-        params: expectedUrlObj.searchParams,
-        responseType: 'text',
-        validateStatus: Communication._validateStatus,
-      };
-      const result = Communication._axiosConfig(method, urlObj);
-      delete result.transformResponse;
-      assert.deepStrictEqual(result, expected);
-    });
-    it('covers data', function () {
-      const method = 'POST';
-      const body = Buffer.from('some data');
-      const params = {};
-      const urlObj = new URL(requestUrl);
-      const expected = {
-        method,
-        url: 'https://example.com/client_id',
-        data: body,
-        headers: {},
-        params: urlObj.searchParams,
-        responseType: 'text',
-        validateStatus: Communication._validateStatus,
-      };
-      const result = Communication._axiosConfig(method, urlObj, body, params, {});
-      delete result.transformResponse;
-      assert.deepStrictEqual(result, expected);
-    });
-    it('covers null response transform', function () {
-      const urlObj = new URL(requestUrl);
-      const result = Communication._axiosConfig('GET', urlObj, undefined, {}, {});
-      result.transformResponse[0]();
-    });
-
-    describe('_validateStatus', function () {
-      it('allows normal valid', function () {
-        const result = Communication._validateStatus(200);
-        assert.strictEqual(result, true);
-      });
-      it('allows unauthorized', function () {
-        const result = Communication._validateStatus(401);
-        assert.strictEqual(result, true);
-      });
-      it('rejects invalid', function () {
-        const result = Communication._validateStatus(400);
-        assert.strictEqual(result, false);
-      });
-    }); // _validateStatus
-  }); // Axios Configurations
-
   describe('_baseUrlString', function () {
     it('covers no path', function () {
       const urlObj = new URL('https://example.com');
@@ -271,7 +184,7 @@ describe('Communication', function () {
         headers: {
           link: '<https://example.com/>; rel="self", <https://hub.example.com/>;rel="hub"',
         },
-        data: {},
+        body: {},
       }
     });
     it('covers', function () {
@@ -358,12 +271,12 @@ describe('Communication', function () {
       urlObj = new URL('https://thuza.ratfeathers.com/');
       response = {
         headers: Object.assign({}, testData.linkHeaders),
-        data: testData.hCardHtml,
+        body: Buffer.from(testData.hCardHtml),
       };
     });
     it('covers', async function () {
-      response.data = testData.hCardHtml;
-      communication.axios.resolves(response);
+      response.body = testData.hCardHtml;
+      communication.got.resolves(response);
       expected = {
         rels: {
           'authorization_endpoint': ['https://ia.squeep.com/auth'],
@@ -410,8 +323,8 @@ describe('Communication', function () {
       result = await communication.fetchMicroformat(urlObj);
       assert.deepStrictEqual(result, expected);
     });
-    it('covers axios error', async function () {
-      communication.axios.rejects(new Error('blah'));
+    it('covers got error', async function () {
+      communication.got.rejects(new Error('blah'));
       expected = undefined;
 
       result = await communication.fetchMicroformat(urlObj);
@@ -419,9 +332,9 @@ describe('Communication', function () {
       assert.deepStrictEqual(result, expected);
     });
     it('covers non-parsable content', async function () {
-      response.data = 'some bare text';
+      response.body = 'some bare text';
       response.headers = {};
-      communication.axios.resolves(response);
+      communication.got.resolves(response);
       expected = {
         items: [],
         rels: {},
@@ -434,7 +347,7 @@ describe('Communication', function () {
     });
     it('covers non-utf8 content', async function () {
       response.headers['content-type'] = 'text/html; charset=ASCII';
-      communication.axios.resolves(response);
+      communication.got.resolves(response);
       expected = {
         rels: {
           'authorization_endpoint': ['https://ia.squeep.com/auth'],
@@ -492,19 +405,19 @@ describe('Communication', function () {
       urlObj = new URL('https://thuza.ratfeathers.com/');
       response = {
         headers: Object.assign({}, testData.linkHeaders),
-        data: testData.hCardHtml,
+        body: testData.hCardHtml,
       };
     });
     it('covers', async function () {
-      communication.axios.resolves(response);
+      communication.got.resolves(response);
       expected = { foo: 'bar', baz: 123 };
-      response.data = JSON.stringify(expected);
+      response.body = expected;
 
       result = await communication.fetchJSON(urlObj);
       assert.deepStrictEqual(result, expected);
     });
-    it('covers axios error', async function () {
-      communication.axios.rejects(new Error('blah'));
+    it('covers got error', async function () {
+      communication.got.rejects(new Error('blah'));
       expected = undefined;
 
       result = await communication.fetchJSON(urlObj);
@@ -512,9 +425,11 @@ describe('Communication', function () {
       assert.deepStrictEqual(result, expected);
     });
     it('covers non-parsable content', async function () {
-      response.data = 'some bare text';
+      response.body = 'some bare text';
       response.headers = {};
-      communication.axios.resolves(response);
+      const error = new Error('oh no');
+      response.request = { options: { url: new URL('https://example.com/') } };
+      communication.got.rejects(new communication.Got.ParseError(error, response));
       expected = undefined;
 
       result = await communication.fetchJSON(urlObj);
@@ -640,11 +555,11 @@ describe('Communication', function () {
       urlObj = new URL('https://thuza.ratfeathers.com/');
       response = {
         headers: {},
-        data: testData.multiMF2Html,
+        body: testData.multiMF2Html,
       };
     });
     it('covers', async function () {
-      communication.axios.resolves(response);
+      communication.got.resolves(response);
       expected = {
         items: [{
           properties: {
@@ -665,14 +580,14 @@ describe('Communication', function () {
       assert.deepStrictEqual(result, expected);
     });
     it('covers failed fetch', async function () {
-      communication.axios.rejects();
+      communication.got.rejects();
       expected = undefined;
       result = await communication.fetchClientIdentifier(urlObj);
       assert.deepStrictEqual(result, expected);
     });
     it('covers no h-app data', async function () {
-      response.data = testData.noneMF2Html;
-      communication.axios.resolves(response);
+      response.body = testData.noneMF2Html;
+      communication.got.resolves(response);
       expected = {
         items: [],
         rels: {},
@@ -729,13 +644,13 @@ describe('Communication', function () {
       urlObj = new URL('https://thuza.ratfeathers.com/');
       response = {
         headers: {},
-        data: testData.hCardHtml,
+        body: testData.hCardHtml,
       };
       sinon.stub(communication, 'fetchJSON');
     });
     describe('legacy without indieauth-metadata', function () {
       it('covers', async function () {
-        communication.axios.resolves(response);
+        communication.got.resolves(response);
         expected = {
           name: 'Thuza',
           photo: 'https://thuza.ratfeathers.com/image.png',
@@ -752,8 +667,8 @@ describe('Communication', function () {
         assert.deepStrictEqual(result, expected);
       });
       it('covers multiple hCards', async function () {
-        response.data = testData.multiMF2Html;
-        communication.axios.resolves(response);
+        response.body = testData.multiMF2Html;
+        communication.got.resolves(response);
         expected = {
           email: undefined,
           name: 'Thuza',
@@ -770,7 +685,7 @@ describe('Communication', function () {
         assert.deepStrictEqual(result, expected);
       });
       it('covers failed fetch', async function () {
-        communication.axios.rejects();
+        communication.got.rejects();
         expected = {
           email: undefined,
           name: undefined,
@@ -783,8 +698,8 @@ describe('Communication', function () {
       });
     });
     it('covers', async function () {
-      response.data = testData.hCardMetadataHtml;
-      communication.axios.resolves(response);
+      response.body = testData.hCardMetadataHtml;
+      communication.got.resolves(response);
       communication.fetchJSON.resolves({
         'issuer': 'https://ia.squeep.com/',
         'authorization_endpoint': 'https://ia.squeep.com/auth',
@@ -828,8 +743,8 @@ describe('Communication', function () {
       assert.deepStrictEqual(result, expected);
     });
     it('covers metadata missing fields', async function () {
-      response.data = testData.hCardMetadataHtml;
-      communication.axios.resolves(response);
+      response.body = testData.hCardMetadataHtml;
+      communication.got.resolves(response);
       communication.fetchJSON.resolves({
         'issuer': 'https://ia.squeep.com/',
       });
@@ -850,8 +765,8 @@ describe('Communication', function () {
     });
     it('covers metadata response failure', async function () {
       const jsonError = new Error('oh no');
-      response.data = testData.hCardMetadataHtml;
-      communication.axios
+      response.body = testData.hCardMetadataHtml;
+      communication.got
         .onCall(0).resolves(response)
         .onCall(1).rejects(jsonError);
       communication.fetchJSON.restore();
@@ -874,14 +789,16 @@ describe('Communication', function () {
     let expected, urlObj, code, codeVerifier, clientId, redirectURI;
     beforeEach(function () {
       urlObj = new URL('https://example.com/auth');
-      code = Buffer.allocUnsafe(42).toString('base64').replace('/', '_').replace('+', '-');
-      codeVerifier = Buffer.allocUnsafe(42).toString('base64').replace('/', '_').replace('+', '-');
+      code = Buffer.allocUnsafe(42).toString('base64url');
+      codeVerifier = Buffer.allocUnsafe(42).toString('base64url');
       clientId = 'https://example.com/';
       redirectURI = 'https://example.com/_ia';
     });
     it('covers', async function () {
-      communication.axios.resolves({
-        data: '{"me":"https://profile.example.com/"}',
+      communication.got.resolves({
+        body: {
+          me: 'https://profile.example.com/',
+        },
       });
       expected = {
         me: 'https://profile.example.com/',
@@ -892,8 +809,10 @@ describe('Communication', function () {
       assert.deepStrictEqual(result, expected);
     });
     it('covers deprecated method name', async function () {
-      communication.axios.resolves({
-        data: '{"me":"https://profile.example.com/"}',
+      communication.got.resolves({
+        body: {
+          me: 'https://profile.example.com/',
+        },
       });
       expected = {
         me: 'https://profile.example.com/',
@@ -904,7 +823,16 @@ describe('Communication', function () {
       assert.deepStrictEqual(result, expected);
     });
     it('covers failure', async function () {
-      communication.axios.resolves('Not a JSON payload.');
+      const error = new Error('oh no');
+      const response = {
+        request: {
+          options: {
+            url: new URL('https://example.com'),
+          },
+        },
+      };
+      const parseError = new communication.Got.ParseError(error, response);
+      communication.got.rejects(parseError);
 
       const result = await communication.redeemCode(urlObj, code, codeVerifier, clientId, redirectURI);
 
@@ -921,30 +849,30 @@ describe('Communication', function () {
     });
     it('covers success active', async function () {
       const nowEpoch = Math.ceil(Date.now() / 1000);
-      communication.axios.resolves({
-        data: JSON.stringify({
+      communication.got.resolves({
+        body: {
           active: true,
           me: 'https://profile.example.com/',
           'client_id': 'https://app.example.com/',
           scope: 'create profile email',
           exp: nowEpoch + 86400,
           iat: nowEpoch,
-        }),
+        },
       });
       const result = await communication.introspectToken(introspectionUrlObj, authenticationHeader, token);
       assert.strictEqual(result.active, true);
     });
     it('covers success inactive', async function () {
-      communication.axios.resolves({
-        data: JSON.stringify({
+      communication.got.resolves({
+        body: {
           active: false,
-        }),
+        },
       });
       const result = await communication.introspectToken(introspectionUrlObj, authenticationHeader, token);
       assert.strictEqual(result.active, false);
     });
     it('covers failure', async function () {
-      communication.axios.resolves('what kind of response is this?');
+      communication.got.resolves({ body: 'what kind of response is this?' });
       await assert.rejects(() => communication.introspectToken(introspectionUrlObj, authenticationHeader, token));
     });
   }); // introspectToken
@@ -958,14 +886,14 @@ describe('Communication', function () {
       ticket = 'XXXThisIsATicketXXX';
     });
     it('covers success', async function () {
-      const expected = { data: 'blah', statusCode: 200 };
-      communication.axios.resolves(expected);
+      const expected = { body: 'blah', statusCode: 200 };
+      communication.got.resolves(expected);
       const result = await communication.deliverTicket(ticketEndpointUrlObj, resourceUrlObj, subjectUrlObj, ticket);
       assert.deepStrictEqual(result, expected);
     });
     it('covers failure', async function () {
       const expectedException = new Error('oh no');
-      communication.axios.rejects(expectedException);
+      communication.got.rejects(expectedException);
       await assert.rejects(() => communication.deliverTicket(ticketEndpointUrlObj, resourceUrlObj, subjectUrlObj, ticket), expectedException);
     });
   }); // deliverTicket