bump package version to 1.4.2
[squeep-indieauth-helper] / lib / communication.js
index 2bda4ce2307dc0b026ba3975e65d0f56a936b940..c1c11285ad668ec864aadc69a678ffe3664811b7 100644 (file)
@@ -4,13 +4,14 @@ 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 { randomBytes, createHash } = require('crypto');
-const { promisify } = require('util');
+const { randomBytes, createHash } = require('node:crypto');
+const { promisify } = require('node:util');
 const randomBytesAsync = promisify(randomBytes);
 const { Address4, Address6 } = require('ip-address');
-const dns = require('dns');
+const dns = require('node:dns');
 const common = require('./common');
 const Enum = require('./enum');
+const { Microformat2: { Relation: MF2Rel } } = Enum;
 const { ValidationError } = require('./errors');
 const { fileScope } = require('@squeep/log-helper');
 
@@ -22,15 +23,21 @@ const loopback4 = new Address4('127.0.0.0/8');
 const scopeSplitRE = / +/;
 const utf8CharsetRE = /utf-*8/i;
 
+/**
+ * @typedef {object} ConsoleLike
+ * @property {Function } debug debug log function
+ * @property {Function } error error log function
+ */
+
 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
-   * @param {String=} options.userAgent.implementation 
+   * @param {ConsoleLike} logger logger
+   * @param {object} options options
+   * @param {number=} options.timeout request timeout
+   * @param {object=} options.userAgent user agent object
+   * @param {string=} options.userAgent.product user agent product
+   * @param {string=} options.userAgent.version user agent version
+   * @param {string=} options.userAgent.implementation user agent implementation
    */
   constructor(logger, options = {}) {
     this.logger = logger;
@@ -46,11 +53,13 @@ class Communication {
 
   /**
    * Do a little dance to support this ESM client.
+   * @param {...any} args request args
+   * @returns {Promise<any>} response
    */
   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: {
@@ -75,8 +84,8 @@ class Communication {
 
   /**
    * Take notes on transient retries.
-   * @param {*} error
-   * @param {*} retryCount
+   * @param {*} error error
+   * @param {*} retryCount retry count
    */
   _onRetry(error, retryCount) {
     const _scope = _fileScope('_onRetry');
@@ -84,10 +93,12 @@ class Communication {
   }
 
 
+  /**
+   * @typedef {string|Buffer|DataView} BinaryLike
   /**
    * Encode hashed verifier data for PKCE.
-   * @param {BinaryLike} verifier
-   * @returns {String}
+   * @param {BinaryLike} verifier verifier
+   * @returns {string} challenge
    */
   static _challengeFromVerifier(verifier) {
     const hash = createHash('sha256');
@@ -98,14 +109,14 @@ class Communication {
 
   /**
    * @typedef PKCEData
-   * @property {String} codeChallengeMethod
-   * @property {String} codeVerifier
-   * @property {String} codeChallenge
+   * @property {string} codeChallengeMethod challenge method
+   * @property {string} codeVerifier code verifier
+   * @property {string} codeChallenge code challenge
    */
   /**
    * Create a code verifier and its challenge.
-   * @param {Number} length of verifier string, between 43 and 128
-   * @returns {Promise<PKCEData>}
+   * @param {number} length of verifier string, between 43 and 128
+   * @returns {Promise<PKCEData>} data
    */
   static async generatePKCE(length = 128) {
     if (length < 43 || length > 128) {
@@ -128,10 +139,10 @@ class Communication {
 
   /**
    * Check a challenge with a verifier.
-   * @param {String} codeChallenge
-   * @param {String} codeVerifier
-   * @param {String} codeChallengeMethod
-   * @returns {Boolean}
+   * @param {string} codeChallenge challenge
+   * @param {string} codeVerifier verifier
+   * @param {string} codeChallengeMethod method
+   * @returns {boolean} is valid
    */
   static verifyChallenge(codeChallenge, codeVerifier, codeChallengeMethod) {
     switch (codeChallengeMethod) {
@@ -149,11 +160,11 @@ class Communication {
 
   /**
    * Assemble a suitable User-Agent value.
-   * @param {Object} userAgentConfig
-   * @param {String=} userAgentConfig.product
-   * @param {String=} userAgentConfig.version
-   * @param {String=} userAgentConfig.implementation 
-   * @returns {String}
+   * @param {object} userAgentConfig user agent object
+   * @param {string=} userAgentConfig.product product
+   * @param {string=} userAgentConfig.version version
+   * @param {string=} userAgentConfig.implementation implementation
+   * @returns {string} UA string
    */
   static _userAgentString(userAgentConfig) {
     // eslint-disable-next-line security/detect-object-injection
@@ -171,8 +182,8 @@ class Communication {
   /**
    * Isolate the base of a url.
    * mf2 parser needs this so that relative links can be made absolute.
-   * @param {URL} urlObj
-   * @returns {String}
+   * @param {URL} urlObj url
+   * @returns {string} url base
    */
   static _baseUrlString(urlObj) {
     const baseUrl = new URL(urlObj);
@@ -184,16 +195,20 @@ class Communication {
   }
 
 
+  /**
+   * @typedef {object} ParsedContentType
+   * @property {string} mediaType media type
+   * @property {object} params other parameters
+   */
   /**
    * Convert a Content-Type string to normalized components.
    * RFC7231 ยง3.1.1
    * N.B. this ill-named non-parsing implementation will not work
    * if a parameter value for some reason includes a ; or = within
    * a quoted-string.
-   * @param {String} contentTypeHeader
-   * @returns {Object} contentType
-   * @returns {String} contentType.mediaType
-   * @returns {Object} contentType.params
+   * @param {string} contentTypeHeader content-type header value
+   * @param {string} defaultContentType default type if none present
+   * @returns {ParsedContentType} contentType 
    */
   static _parseContentType(contentTypeHeader, defaultContentType = Enum.ContentType.ApplicationOctetStream) {
     const [ mediaType, ...params ] = (contentTypeHeader || '').split(/ *; */);
@@ -211,9 +226,9 @@ class Communication {
 
   /**
    * Parse and add any header link relations from response to microformat data.
-   * @param {Object} microformat
-   * @param {Object} response
-   * @param {Object} response.headers
+   * @param {object} microformat microformat
+   * @param {object} response response
+   * @param {object} response.headers response headers
    */
   _mergeLinkHeader(microformat, response) {
     const _scope = _fileScope('_mergeLinkHeader');
@@ -265,8 +280,8 @@ class Communication {
   /**
    * Retrieve and parse microformat data from url.
    * N.B. this absorbs any errors!
-   * @param {URL} urlObj
-   * @returns {Promise<Object>}
+   * @param {URL} urlObj url
+   * @returns {Promise<object>} microformat
    */
   async fetchMicroformat(urlObj) {
     const _scope = _fileScope('fetchMicroformat');
@@ -329,8 +344,8 @@ class Communication {
   /**
    * Retrieve and parse JSON.
    * N.B. this absorbs any errors!
-   * @param {URL} urlObj
-   * @returns {Promise<Object>}
+   * @param {URL} urlObj url
+   * @returns {Promise<object>} parsed json
    */
   async fetchJSON(urlObj) {
     const _scope = _fileScope('fetchJSON');
@@ -360,8 +375,8 @@ class Communication {
 
   /**
    * Validate a url has a specific schema.
-   * @param {URL} urlObj
-   * @param {String[]} validSchemes
+   * @param {URL} urlObj url
+   * @param {string[]} validSchemes url schemes
    */
   static _urlValidScheme(urlObj, validSchemes = ['http:', 'https:']) {
     if (!validSchemes.includes(urlObj.protocol)) {
@@ -372,8 +387,8 @@ class Communication {
 
   /**
    * Validate a url does not include some components.
-   * @param {URL} urlObj
-   * @param {String[]} disallowed
+   * @param {URL} urlObj url
+   * @param {string[]} disallowed component names
    */
   static _urlPartsDisallowed(urlObj, disallowed) {
     disallowed.forEach((part) => {
@@ -386,7 +401,7 @@ class Communication {
 
   /**
    * Validate a url does not have relative path.
-   * @param {String} url
+   * @param {string} url url
    */
   static _urlPathNoDots(url) {
     if (noDotPathRE.test(url)) {
@@ -398,9 +413,9 @@ class Communication {
   /**
    * Validate a url does not have a hostname which is an ip address.
    * N.B. Sets isLoopback on urlObj
-   * @param {URL} urlObj
-   * @param {Boolean} allowLoopback
-   * @param {Boolean} resolveHostname
+   * @param {URL} urlObj url
+   * @param {boolean} allowLoopback allow loopback ip
+   * @param {boolean} resolveHostname resolve hostname for checks
    * @returns {Promise<void>}
    */
   static async _urlNamedHost(urlObj, allowLoopback, resolveHostname) {
@@ -412,15 +427,15 @@ class Communication {
        * call would already have failed.
        */
       address = new Address6(urlObj.hostname.slice(1, urlObj.hostname.length - 1));
-      /* succeeded parsing as ipv6, reject unless loopback */
+      /* Succeeded parsing as ipv6, reject unless loopback */
       urlObj.isLoopback = address.isLoopback();
     } else {
       try {
         address = new Address4(urlObj.hostname);
-        /* succeeded parsing as ipv4, reject unless loopback */
+        /* Succeeded parsing as ipv4, reject unless loopback */
         urlObj.isLoopback = address.isInSubnet(loopback4);
-      } catch (e) {
-        /* did not parse as ip, carry on */
+      } catch (e) { // eslint-disable-line no-unused-vars
+        /* Did not parse as ip, carry on */
       }
     }
 
@@ -447,12 +462,12 @@ class Communication {
         throw new ValidationError('could not resolve hostname');
       }
 
-      // extract each resolution value, array of {address,family}
+      // Extract each resolution value, array of {address,family}
       const resolutions = settledResolutions.map((resolution) => resolution.value);
 
       // If there were two resolutions, ensure they returned identical results.
       if (resolutions.length > 1) {
-        // create set of addresses for each resolution
+        // Create set of addresses for each resolution
         const addressSets = resolutions.map((addrs) => {
           return new Set((addrs || []).map((a) => a.address));
         });
@@ -488,20 +503,21 @@ class Communication {
 
   /**
    * Ensure a url meets the requirements to be a profile uri.
-   * @param {String} url
-   * @param {Object} validationOptions
-   * @param {Boolean=} validationOptions.allowLoopback default is false, following spec
-   * @param {Boolean=} validationOptions.resolveHostname default is false, following spec
-   * @returns {Promise<URL>}
+   * @param {string} url url
+   * @param {object} validationOptions options
+   * @param {boolean=} validationOptions.allowLoopback default is false, following spec
+   * @param {boolean=} validationOptions.resolveHostname default is false, following spec
+   * @returns {Promise<URL>} validated url
    */
   async validateProfile(url, validationOptions) {
     const _scope = _fileScope('validateProfile');
     const errorScope = 'invalid profile url';
 
-    const options = Object.assign({
+    const options = {
       allowLoopback: false,
       resolveHostname: false,
-    }, validationOptions);
+      ...validationOptions,
+    };
 
     let profile;
     try {
@@ -529,20 +545,21 @@ class Communication {
   /**
    * Ensure a url meets the requirements to be a client identifier.
    * Sets 'isLoopback' on returned URL object to true if hostname is - or resolves to - a loopback ip.
-   * @param {String} url
-   * @param {Object} validationOptions
-   * @param {Boolean=} validationOptions.allowLoopback default is true, following spec
-   * @param {Boolean=} validationOptions.resolveHostname default is true, following spec
-   * @returns {Promise<URL>}
+   * @param {string} url url
+   * @param {object} validationOptions options
+   * @param {boolean=} validationOptions.allowLoopback default is true, following spec
+   * @param {boolean=} validationOptions.resolveHostname default is true, following spec
+   * @returns {Promise<URL>} validated client url
    */
   async validateClientIdentifier(url, validationOptions) {
     const _scope = _fileScope('validateClientIdentifier');
     const errorScope = 'invalid client identifier url';
 
-    const options = Object.assign({
+    const options = {
       allowLoopback: true,
       resolveHostname: true,
-    }, validationOptions);
+      ...validationOptions,
+    };
 
     let clientId;
     try {
@@ -568,28 +585,29 @@ class Communication {
 
 
   /**
-   * @typedef {Object} ClientIdentifierData
-   * @property {Object} rels - keyed by relation to array of uris
-   * @property {HAppData[]} items
+   * @typedef {object} HAppData
+   *  @property {string[]=} type list of item types
+   *  @property {object=} properties map of item properties
+   */
+  /**
+   * @typedef {object} ClientIdentifierData
+   * @property {object} rels keyed by relation to array of uris
+   * @property {HAppData[]} items h-app data
    */
   /**
    * Retrieve and parse client identifier endpoint data.
    * N.B. Assumes urlObj has passed validateClientIdentifier.
-   * @param {URL} urlObj
+   * @param {URL} urlObj url
    * @returns {Promise<ClientIdentifierData|undefined>} mf2 data filtered for h-app items, or undefined if url could not be fetched
    */
   async fetchClientIdentifier(urlObj) {
-    const _scope = _fileScope('fetchClientIdentifier');
-
-    // Loopback address will eschew client fetch, return empty data.
-    const isLoopbackResult = {
-      rels: {},
-      items: [],
-    };
-
     // Set by validation method in case of loopback ip hostname
     if (urlObj.isLoopback) {
-      return isLoopbackResult;
+      // Loopback address will eschew client fetch, return empty data.
+      return {
+        rels: {},
+        items: [],
+      };
     }
 
     const mfData = await this.fetchMicroformat(urlObj);
@@ -611,7 +629,7 @@ class Communication {
               if (hUrl.href === urlObj.href) {
                 urlMatched = true;
               }
-            } catch (e) { /**/ }
+            } catch (e) { /**/ } // eslint-disable-line no-unused-vars
           });
         }
         return urlMatched;
@@ -621,39 +639,39 @@ class Communication {
 
 
   /**
-   * @typedef {Object} Metadata
-   * @property {String} issuer
-   * @property {String} authorizationEndpoint
-   * @property {String} tokenEndpoint
-   * @property {String} ticketEndpoint
-   * @property {String} introspectionEndpoint
-   * @property {String} introspectionEndpointAuthMethodsSupported
-   * @property {String} revocationEndpoint
-   * @property {String} revocationEndpointAuthMethodsSupported
-   * @property {String} scopesSupported
-   * @property {String} responseTypesSupported
-   * @property {String} grantTypesSupported
-   * @property {String} serviceDocumentation
-   * @property {String} codeChallengeMethodsSupported
-   * @property {String} authorizationResponseIssParameterSupported
-   * @property {String} userinfoEndpoint
+   * @typedef {object} Metadata
+   * @property {string} issuer issuer
+   * @property {string} authorizationEndpoint authorization endpoint
+   * @property {string} tokenEndpoint token endpoint
+   * @property {string} ticketEndpoint ticket endpoint
+   * @property {string} introspectionEndpoint introspection endpoint
+   * @property {string} introspectionEndpointAuthMethodsSupported auth methods
+   * @property {string} revocationEndpoint revocation endpoint
+   * @property {string} revocationEndpointAuthMethodsSupported auth methods
+   * @property {string} scopesSupported scopes
+   * @property {string} responseTypesSupported response types
+   * @property {string} grantTypesSupported grant types
+   * @property {string} serviceDocumentation service documentation reference
+   * @property {string} codeChallengeMethodsSupported code challenge methods
+   * @property {string} authorizationResponseIssParameterSupported iss parameter supported
+   * @property {string} userinfoEndpoint endpoint
    */
   /**
    * @typedef ProfileData
-   * @property {String} name
-   * @property {String} photo
-   * @property {String} url
-   * @property {String} email
-   * @property {String} authorizationEndpoint - deprecated, backwards compatibility for 20201126 spec
-   * @property {String} tokenEndpoint - deprecated, backwards compatibility for 20201126 spec
-   * @property {String} indieauthMetadata authorization server metadata endpoint
-   * @property {Metadata} metadata authorization server metadata for profile
+   * @property {string} name name
+   * @property {string} photo photo
+   * @property {string} url url
+   * @property {string} email email
+   * @property {string} authorizationEndpoint deprecated, backwards compatibility for 20201126 spec
+   * @property {string} tokenEndpoint deprecated, backwards compatibility for 20201126 spec
+   * @property {string} indieauthMetadata authorization server metadata endpoint
+   * @property {Metadata} metadata authorization server metadata for profile
    */
   /**
    * Fetch the relevant microformat data from profile url h-card information,
    * and authorization server metadata.
    * N.B. Assumes urlObj has passed validateProfile
-   * @param {URL} urlObj
+   * @param {URL} urlObj valid profile url
    * @returns {Promise<ProfileData>} mf2 data filtered for select fields from h-card
    */
   async fetchProfile(urlObj) {
@@ -668,12 +686,14 @@ class Communication {
       metadata: {},
     };
 
-    // Locate h-card mf2 items with url field matching profile url,
-    // and populate profile fields with first-encountered card values.
+    /**
+     * Locate h-card mf2 items with url field matching profile url,
+     * and populate profile fields with first-encountered card values.
+     */
     if (mfData && 'items' in mfData) {
       const hCards = mfData.items.filter((item) =>
         item?.type?.includes('h-card') &&
-        item.properties && item.properties.url && item.properties.url.includes(urlObj.href));
+        item?.properties?.url?.includes(urlObj.href));
       hCards.forEach((hCard) => {
         Object.keys(profile).forEach((key) => {
           if (!profile[key] && key in hCard.properties) { // eslint-disable-line security/detect-object-injection
@@ -686,9 +706,9 @@ class Communication {
     // Populate legacy mf2 fields from relation links.
     // These will be overwritten if they also exist in server metadata.
     Object.entries({
-      authorizationEndpoint: 'authorization_endpoint', // backwards compatibility
-      tokenEndpoint: 'token_endpoint', // backwards compatibility
-      ticketEndpoint: 'ticket_endpoint', // backwards compatibility
+      authorizationEndpoint: MF2Rel.AuthorizationEndpoint, // Backwards compatibility
+      tokenEndpoint: MF2Rel.TokenEndpoint, // Backwards compatibility
+      ticketEndpoint: MF2Rel.TicketEndpoint, // Backwards compatibility
     }).forEach(([p, r]) => {
       if (mfData && r in mfData.rels) {
         profile.metadata[p] = profile[p] = mfData.rels[r][0]; // eslint-disable-line security/detect-object-injection
@@ -696,8 +716,8 @@ class Communication {
     });
 
     // Set metadata field.
-    if (mfData && 'indieauth-metadata' in mfData.rels) {
-      profile.indieauthMetadata = mfData.rels['indieauth-metadata'][0];
+    if (mfData && MF2Rel.IndieauthMetadata in mfData.rels) {
+      profile.indieauthMetadata = mfData.rels[MF2Rel.IndieauthMetadata][0];
     }
 
     // Attempt to populate metadata from authorization server.
@@ -705,7 +725,7 @@ class Communication {
       let mdURL;
       try {
         mdURL = new URL(profile.indieauthMetadata);
-      } catch (e) /* istanbul ignore next */ {
+      } catch (e) /* istanbul ignore next */ { // eslint-disable-line no-unused-vars
         this.logger.error(_scope, 'invalid authorization server metadata url', { profile });
       }
       /* istanbul ignore else */
@@ -727,8 +747,8 @@ class Communication {
 
   /**
    * Fetch the server metadata from an authorization server's metadata endpoint.
-   * @param {URL} metadataUrl
-   * @returns {Promise<Metadata>}
+   * @param {URL} metadataUrl url
+   * @returns {Promise<Metadata>} metadata
    */
   async fetchMetadata(metadataUrl) {
     const metadataResponse = await this.fetchJSON(metadataUrl);
@@ -765,12 +785,12 @@ class Communication {
   /**
    * POST to the auth endpoint, to redeem a code for a profile or token.
    * N.B. this absorbs any errors!
-   * @param {URL} urlObj
-   * @param {String} code
-   * @param {String} codeVerifier
-   * @param {String} clientId
-   * @param {String} redirectURI
-   * @returns {Promise<Object>}
+   * @param {URL} urlObj url
+   * @param {string} code code
+   * @param {string} codeVerifier verifier
+   * @param {string} clientId client id
+   * @param {string} redirectURI uri
+   * @returns {Promise<object>} response
    */
   async redeemCode(urlObj, code, codeVerifier, clientId, redirectURI) {
     const _scope = _fileScope('redeemCode');
@@ -804,12 +824,12 @@ class Communication {
   /**
    * Deprecated method name alias.
    * @see redeemCode
-   * @param {URL} urlObj
-   * @param {String} code
-   * @param {Strin} codeVerifier
-   * @param {String} clientId
-   * @param {String} redirectURI
-   * @returns {Promise<Object>}
+   * @param {URL} urlObj url
+   * @param {string} code code
+   * @param {string} codeVerifier verifier
+   * @param {string} clientId client id
+   * @param {string} redirectURI uri
+   * @returns {Promise<object>} response
    */
   async redeemProfileCode(urlObj, code, codeVerifier, clientId, redirectURI) {
     return this.redeemCode(urlObj, code, codeVerifier, clientId, redirectURI);
@@ -818,10 +838,10 @@ class Communication {
 
   /**
    * Verify a token with an IdP endpoint, using the Authorization header supplied.
-   * @param {URL} introspectionUrlObj
-   * @param {String} authorizationHeader
-   * @param {String} token
-   * @returns {Promise<Object>}
+   * @param {URL} introspectionUrlObj url
+   * @param {string} authorizationHeader authorization
+   * @param {string} token token
+   * @returns {Promise<object>} response
    */
   async introspectToken(introspectionUrlObj, authorizationHeader, token) {
     const _scope = _fileScope('introspectToken');
@@ -841,7 +861,7 @@ class Communication {
 
     try {
       const response = await this.got(postIntrospectConfig);
-      // check status
+      // Check status
       try {
         const {
           active,
@@ -878,12 +898,12 @@ class Communication {
   /**
    * Attempt to deliver a ticket to an endpoint.
    * N.B. does not absorb errors
-   * @param {URL} ticketEndpointUrlObj
-   * @param {URL} resourceUrlObj
-   * @param {URL} subjectUrlObj
-   * @param {URL=} issuerUrlObj
-   * @param {String} ticket
-   * @returns {Promise<Response>}
+   * @param {URL} ticketEndpointUrlObj url ticket endpoint url
+   * @param {URL} resourceUrlObj url resource url
+   * @param {URL} subjectUrlObj url subject url
+   * @param {string} ticket ticket
+   * @param {URL=} issuerUrlObj issuer url
+   * @returns {Promise<Response>} response
    */
   async deliverTicket(ticketEndpointUrlObj, resourceUrlObj, subjectUrlObj, ticket, issuerUrlObj) {
     const _scope = _fileScope('deliverTicket');
@@ -911,8 +931,8 @@ class Communication {
 
   /**
    * Attempt to fetch some link relations from a url.
-   * @param {URL} urlObj
-   * @returns {Promise<Object>}
+   * @param {URL} urlObj url
+   * @returns {Promise<object>} data
    */
   async _fetchMetadataOrTokenEndpoint(urlObj) {
     const _scope = _fileScope('_fetchMetadataOrTokenEndpoint');
@@ -920,21 +940,21 @@ class Communication {
     let metadataUrl, tokenUrl;
     if (urlObj) {
       const mfData = await this.fetchMicroformat(urlObj);
-      const metadataRel = mfData?.rels?.['indieauth-metadata']?.[0];
+      const metadataRel = mfData?.rels?.[MF2Rel.IndieauthMetadata]?.[0];
       if (metadataRel) {
         try {
           metadataUrl = new URL(metadataRel);
-        } catch (e) {
+        } catch (e) { // eslint-disable-line no-unused-vars
           this.logger.debug(_scope, 'invalid metadata rel url', { url: urlObj.href, metadataRel });
         }
       }
       if (!metadataUrl) {
-        // no metadata rel, try old-style token endpoint
-        const tokenRel = mfData?.rels?.['token_endpoint']?.[0];
+        // No metadata rel, try old-style token endpoint
+        const tokenRel = mfData?.rels?.[MF2Rel.TokenEndpoint]?.[0];
         if (tokenRel) {
           try {
             tokenUrl = new URL(tokenRel);
-          } catch (e) {
+          } catch (e) { // eslint-disable-line no-unused-vars
             this.logger.debug(_scope, 'invalid token rel url', { url: urlObj.href, tokenRel });
           }
         }
@@ -947,10 +967,10 @@ class Communication {
   /**
    * Attempt to redeem a ticket for a token.
    * N.B. does not absorb errors
-   * @property {String} ticket
-   * @property {URL} resourceUrlObj
-   * @property {URL=} issuerUrlObj
-   * @returns {Promise<Object>} response body
+   * @param {string} ticket ticket
+   * @param {URL} resourceUrlObj resource url
+   * @param {URL=} issuerUrlObj issuer url
+   * @returns {Promise<object>} response body
    */
   async redeemTicket(ticket, resourceUrlObj, issuerUrlObj) {
     const _scope = _fileScope('redeemTicket');
@@ -970,7 +990,7 @@ class Communication {
       const metadata = await this.fetchMetadata(metadataUrl);
       try {
         tokenUrl = new URL(metadata?.tokenEndpoint);
-      } catch (e) {
+      } catch (e) { // eslint-disable-line no-unused-vars
         this.logger.debug(_scope, 'invalid token endpoint url from metadata', { resourceUrl: resourceUrlObj.href, issuerUrl: issuerUrlObj.href, tokenEndpoint: metadata?.tokenEndpoint });
       }
     }