support indieauth-metadata relation on profiles, per spec 20220212
authorJustin Wind <justin.wind+git@gmail.com>
Fri, 18 Feb 2022 19:20:53 +0000 (11:20 -0800)
committerJustin Wind <justin.wind+git@gmail.com>
Fri, 18 Feb 2022 19:29:09 +0000 (11:29 -0800)
README.md
lib/communication.js
lib/enum.js
lib/utility.js [deleted file]
package-lock.json
package.json
test/lib/communication.js
test/test-data/communication.js

index dee619d5bdf2c32e5332f2581e0e5888dd062b47..bd005a7e83b23355752f82d1e78e9e0d6845d93c 100644 (file)
--- a/README.md
+++ b/README.md
@@ -4,14 +4,17 @@ Just some abstractions for interacting with IndieAuth sites.
 
 Notable methods on the Communication class:
 
-- static generatePKCE(length)  
+- `static async generatePKCE(length)`  
   Create a code and verifier for use in an IndieAuth transaction.
 
-- fetchProfile(urlObject)  
+- `async fetchProfile(urlObject)`  
   Retrieve profile information from an endpoint.
 
-- fetchClientIdentifier(urlObject)  
+- `async fetchClientIdentifier(urlObject)`  
   Retrieve client identifier information from an endpoint.
 
-- fetchMicroformat(urlObject)  
-  Retrieve all mf2 information form an endpoint.
+- `async fetchMicroformat(urlObject)`  
+  Retrieve all mf2 information from an endpoint.
+
+- `async fetchJSON(urlObject)`  
+  Retrieve json from an endpoint.
index 51f5b430c9fc160687c07111f1fe4727e759800b..2b0c621388aa322a599766064a0658afba50c97d 100644 (file)
@@ -291,6 +291,41 @@ class Communication {
   }
 
 
+  /**
+   * Retrieve and parse JSON.
+   * N.B. this absorbs any errors!
+   * @param {URL} urlObj
+   * @returns {Object}
+   */
+  async fetchJSON(urlObj) {
+    const _scope = _fileScope('fetchJSON');
+    const logInfoData = {
+      url: urlObj.href,
+      response: undefined,
+    };
+    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);
+    } catch (e) {
+      this.logger.error(_scope, 'json request failed', { error: e, ...logInfoData });
+      return;
+    }
+    logInfoData.response = common.axiosResponseLogData(response);
+
+    let data;
+    try {
+      data = JSON.parse(response.data);
+    } catch (e) {
+      this.logger.error(_scope, 'json parsing failed', { error: e, ...logInfoData });
+    }
+
+    return data;
+  }
+
+
   /**
    * @typedef {Object} ClientIdentifierData
    * @property {Object} rels - keyed by relation to array of uris
@@ -336,23 +371,45 @@ class Communication {
    * @property {String} photo
    * @property {String} url
    * @property {String} email
-   * @property {String} authorizationEndpoint
-   * @property {String} tokenEndpoint
+   * @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 {Object} metadata - authorization server metadata for profile
+   * @property {String} metadata.issuer
+   * @property {String} metadata.authorizationEndpoint
+   * @property {String} metadata.tokenEndpoint
+   * @property {String} metadata.introspectionEndpoint
+   * @property {String} metadata.introspectionEndpointAuthMethodsSupported
+   * @property {String} metadata.revocationEndpoint
+   * @property {String} metadata.revocationEndpointAuthMethodsSupported
+   * @property {String} metadata.scopesSupported
+   * @property {String} metadata.responseTypesSupported
+   * @property {String} metadata.grantTypesSupported
+   * @property {String} metadata.serviceDocumentation
+   * @property {String} metadata.codeChallengeMethodsSupported
+   * @property {String} metadata.authorizationResponseIssParameterSupported
+   * @property {String} metadata.userinfoEndpoint
    */
   /**
-   * Fetch all the microformat data from url, but only return the relevant h-card profile information.
+   * Fetch the relevant microformat data from profile url h-card information,
+   * and authorization server metadata.
    * @param {URL} urlObj
    * @returns {ProfileData} mf2 data filtered for select fields from h-card
    */
   async fetchProfile(urlObj) {
+    const _scope = _fileScope('fetchProfile');
+
     const mfData = await this.fetchMicroformat(urlObj);
     const profile = {
       name: undefined,
       photo: undefined,
       url: undefined,
       email: undefined,
+      metadata: {},
     };
 
+    // 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 && item.type.includes('h-card') &&
@@ -365,15 +422,67 @@ 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',
-      tokenEndpoint: 'token_endpoint',
+      authorizationEndpoint: 'authorization_endpoint', // backwards compatibility
+      tokenEndpoint: 'token_endpoint', // backwards compatibility
     }).forEach(([p, r]) => {
       if (mfData && r in mfData.rels) {
-        profile[p] = mfData.rels[r][0]; // eslint-disable-line security/detect-object-injection
+        profile.metadata[p] = profile[p] = mfData.rels[r][0]; // eslint-disable-line security/detect-object-injection
       }
     });
 
+    // Set metadata field.
+    if (mfData && 'indieauth-metadata' in mfData.rels) {
+      profile.indieauthMetadata = mfData.rels['indieauth-metadata'][0];
+    }
+
+    // Attempt to populate metadata from authorization server.
+    if (profile.indieauthMetadata) {
+      let mdURL;
+      try {
+        mdURL = new URL(profile.indieauthMetadata);
+      } catch (e) /* istanbul ignore next */ {
+        this.logger.error(_scope, 'invalid authorization server metadata url', { profile });
+      }
+      /* istanbul ignore else */
+      if (mdURL) {
+        const metadataResponse = await this.fetchJSON(mdURL);
+        if (metadataResponse) {
+          // Map snake_case fields to camelCase.
+          Object.entries({
+            issuer: 'issuer',
+            authorizationEndpoint: 'authorization_endpoint',
+            tokenEndpoint: 'token_endpoint',
+            introspectionEndpoint: 'introspection_endpoint',
+            introspectionEndpointAuthMethodsSupported: 'introspection_endpoint_auth_methods_supported',
+            revocationEndpoint: 'revocation_endpoint',
+            revocationEndpointAuthMethodsSupported: 'revocation_endpoint_auth_methods_supported',
+            scopesSupported: 'scopes_supported',
+            responseTypesSupported: 'response_types_supported',
+            grantTypesSupported: 'grant_types_supported',
+            serviceDocumentation: 'service_documentation',
+            codeChallengeMethodsSupported: 'code_challenge_methods_supported',
+            authorizationResponseIssParameterSupported: 'authorization_response_iss_parameter_supported',
+            userinfoEndpoint: 'userinfo_endpoint',
+          }).forEach(([c, s]) => {
+            if (s in metadataResponse) {
+              profile.metadata[c] = metadataResponse[s]; // eslint-disable-line security/detect-object-injection
+            }
+          });
+
+          // Populate legacy profile fields.
+          ['authorizationEndpoint', 'tokenEndpoint'].forEach((f) => {
+            if (f in profile.metadata) {
+              profile[f] = profile.metadata[f]; // eslint-disable-line security/detect-object-injection
+            }
+          });
+        }
+      }
+    }
+
     return profile;
   }
 
index ef5c0ff5411b3637adc2e253d39908a06cc2144e..dcec1310229a72f00b8da3460c8d3c407c9f46db 100644 (file)
@@ -1,7 +1,7 @@
 'use strict';
 
 module.exports = {
-  Specification: 'living-standard-20201126',
+  Specification: 'living-standard-20220212',
 
   ContentType: {
     Any: '*/*',
diff --git a/lib/utility.js b/lib/utility.js
deleted file mode 100644 (file)
index e69de29..0000000
index 81b536725285faea7e8ca8b2f8b4b0e9b2939d7d..049166fad1c0076479945d4d6011a8915b48f5e3 100644 (file)
       }
     },
     "@eslint/eslintrc": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.5.tgz",
-      "integrity": "sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ==",
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.1.0.tgz",
+      "integrity": "sha512-C1DfL7XX4nPqGd6jcP01W9pVM1HYCuUkFk1432D7F0v3JSlUIeOYn9oCoi3eoLZ+iwBSb29BMFxxny0YrrEZqg==",
       "dev": true,
       "requires": {
         "ajv": "^6.12.4",
         "debug": "^4.3.2",
-        "espree": "^9.2.0",
+        "espree": "^9.3.1",
         "globals": "^13.9.0",
         "ignore": "^4.0.6",
         "import-fresh": "^3.2.1",
       }
     },
     "@humanwhocodes/config-array": {
-      "version": "0.9.2",
-      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.2.tgz",
-      "integrity": "sha512-UXOuFCGcwciWckOpmfKDq/GyhlTf9pN/BzG//x8p8zTOFEcGuA68ANXheFS0AGvy3qgZqLBUkMs7hqzqCKOVwA==",
+      "version": "0.9.3",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.3.tgz",
+      "integrity": "sha512-3xSMlXHh03hCcCmFc0rbKp3Ivt2PFEJnQUJDDMTJQ2wkECZWdq4GePs2ctc5H8zV+cHPaq8k2vU8mrQjA6iHdQ==",
       "dev": true,
       "requires": {
         "@humanwhocodes/object-schema": "^1.2.1",
       }
     },
     "@sinonjs/fake-timers": {
-      "version": "8.1.0",
-      "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz",
-      "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==",
+      "version": "9.0.0",
+      "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.0.0.tgz",
+      "integrity": "sha512-+shXA2X7KNP7H7qNbQTJ3SA+NQc0pZDSBrdvFSRwF8sAo/ohw+ZQFD8Moc+gnz51+1eRXtEQBpKWPiQ4jsRC/w==",
       "dev": true,
       "requires": {
         "@sinonjs/commons": "^1.7.0"
       }
     },
     "@sinonjs/samsam": {
-      "version": "6.0.2",
-      "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.0.2.tgz",
-      "integrity": "sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==",
+      "version": "6.1.1",
+      "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.1.1.tgz",
+      "integrity": "sha512-cZ7rKJTLiE7u7Wi/v9Hc2fs3Ucc3jrWeMgPHbbTCeVAB2S0wOBbYlkJVeNSL04i7fdhT8wIbDq1zhC/PXTD2SA==",
       "dev": true,
       "requires": {
         "@sinonjs/commons": "^1.6.0",
       }
     },
     "axios": {
-      "version": "0.25.0",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-0.25.0.tgz",
-      "integrity": "sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g==",
+      "version": "0.26.0",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz",
+      "integrity": "sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og==",
       "requires": {
-        "follow-redirects": "^1.14.7"
+        "follow-redirects": "^1.14.8"
       }
     },
     "balanced-match": {
       }
     },
     "chokidar": {
-      "version": "3.5.2",
-      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
-      "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==",
+      "version": "3.5.3",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
+      "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
       "dev": true,
       "requires": {
         "anymatch": "~3.1.2",
       "dev": true
     },
     "eslint": {
-      "version": "8.7.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.7.0.tgz",
-      "integrity": "sha512-ifHYzkBGrzS2iDU7KjhCAVMGCvF6M3Xfs8X8b37cgrUlDt6bWRTpRh6T/gtSXv1HJ/BUGgmjvNvOEGu85Iif7w==",
+      "version": "8.9.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.9.0.tgz",
+      "integrity": "sha512-PB09IGwv4F4b0/atrbcMFboF/giawbBLVC7fyDamk5Wtey4Jh2K+rYaBhCAbUyEI4QzB1ly09Uglc9iCtFaG2Q==",
       "dev": true,
       "requires": {
-        "@eslint/eslintrc": "^1.0.5",
+        "@eslint/eslintrc": "^1.1.0",
         "@humanwhocodes/config-array": "^0.9.2",
         "ajv": "^6.10.0",
         "chalk": "^4.0.0",
         "debug": "^4.3.2",
         "doctrine": "^3.0.0",
         "escape-string-regexp": "^4.0.0",
-        "eslint-scope": "^7.1.0",
+        "eslint-scope": "^7.1.1",
         "eslint-utils": "^3.0.0",
-        "eslint-visitor-keys": "^3.2.0",
-        "espree": "^9.3.0",
+        "eslint-visitor-keys": "^3.3.0",
+        "espree": "^9.3.1",
         "esquery": "^1.4.0",
         "esutils": "^2.0.2",
         "fast-deep-equal": "^3.1.3",
       }
     },
     "eslint-plugin-sonarjs": {
-      "version": "0.11.0",
-      "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.11.0.tgz",
-      "integrity": "sha512-ei/WuZiL0wP+qx2KrxKyZs3+eDbxiGAhFSm3GKCOOAUkg+G2ny6TSXDB2j67tvyqHefi+eoQsAgGQvz+nEtIBw==",
+      "version": "0.12.0",
+      "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.12.0.tgz",
+      "integrity": "sha512-soxjK67hoYxO8hesKqXWN50GSM+oG2r35N5WnAMehetahO6zoVpv3HZbdziP0jYWNopEe6te/BFUZOYAZI+qhg==",
       "dev": true
     },
     "eslint-scope": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz",
-      "integrity": "sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==",
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
+      "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==",
       "dev": true,
       "requires": {
         "esrecurse": "^4.3.0",
       }
     },
     "eslint-visitor-keys": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz",
-      "integrity": "sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ==",
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz",
+      "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==",
       "dev": true
     },
     "espree": {
-      "version": "9.3.0",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.0.tgz",
-      "integrity": "sha512-d/5nCsb0JcqsSEeQzFZ8DH1RmxPcglRWh24EFTlUEmCKoehXGdpsx0RkHDubqUI8LSAIKMQp4r9SzQ3n+sm4HQ==",
+      "version": "9.3.1",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz",
+      "integrity": "sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==",
       "dev": true,
       "requires": {
         "acorn": "^8.7.0",
         "acorn-jsx": "^5.3.1",
-        "eslint-visitor-keys": "^3.1.0"
+        "eslint-visitor-keys": "^3.3.0"
       }
     },
     "esprima": {
       }
     },
     "flatted": {
-      "version": "3.2.4",
-      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz",
-      "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==",
+      "version": "3.2.5",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz",
+      "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
       "dev": true
     },
     "follow-redirects": {
-      "version": "1.14.7",
-      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz",
-      "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ=="
+      "version": "1.14.8",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
+      "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA=="
     },
     "foreground-child": {
       "version": "2.0.0",
       }
     },
     "globals": {
-      "version": "13.12.0",
-      "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz",
-      "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==",
+      "version": "13.12.1",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz",
+      "integrity": "sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw==",
       "dev": true,
       "requires": {
         "type-fest": "^0.20.2"
       "dev": true
     },
     "mocha": {
-      "version": "9.1.4",
-      "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.4.tgz",
-      "integrity": "sha512-+q2aV5VlJZuLgCWoBvGI5zEwPF9eEI0kr/sAA9Jm4xMND7RfIEyF8JE7C0JIg8WXRG+P1sdIAb5ccoHPlXLzcw==",
+      "version": "9.2.0",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.0.tgz",
+      "integrity": "sha512-kNn7E8g2SzVcq0a77dkphPsDSN7P+iYkqE0ZsGCYWRsoiKjOt+NvXfaagik8vuDa6W5Zw3qxe8Jfpt5qKf+6/Q==",
       "dev": true,
       "requires": {
         "@ungap/promise-all-settled": "1.1.2",
         "ansi-colors": "4.1.1",
         "browser-stdout": "1.3.1",
-        "chokidar": "3.5.2",
-        "debug": "4.3.2",
+        "chokidar": "3.5.3",
+        "debug": "4.3.3",
         "diff": "5.0.0",
         "escape-string-regexp": "4.0.0",
         "find-up": "5.0.0",
-        "glob": "7.1.7",
+        "glob": "7.2.0",
         "growl": "1.10.5",
         "he": "1.2.0",
         "js-yaml": "4.1.0",
         "log-symbols": "4.1.0",
         "minimatch": "3.0.4",
         "ms": "2.1.3",
-        "nanoid": "3.1.25",
+        "nanoid": "3.2.0",
         "serialize-javascript": "6.0.0",
         "strip-json-comments": "3.1.1",
         "supports-color": "8.1.1",
         "which": "2.0.2",
-        "workerpool": "6.1.5",
+        "workerpool": "6.2.0",
         "yargs": "16.2.0",
         "yargs-parser": "20.2.4",
         "yargs-unparser": "2.0.0"
           "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
           "dev": true
         },
-        "glob": {
-          "version": "7.1.7",
-          "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz",
-          "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==",
+        "debug": {
+          "version": "4.3.3",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz",
+          "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==",
           "dev": true,
           "requires": {
-            "fs.realpath": "^1.0.0",
-            "inflight": "^1.0.4",
-            "inherits": "2",
-            "minimatch": "^3.0.4",
-            "once": "^1.3.0",
-            "path-is-absolute": "^1.0.0"
+            "ms": "2.1.2"
+          },
+          "dependencies": {
+            "ms": {
+              "version": "2.1.2",
+              "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+              "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+              "dev": true
+            }
           }
         },
         "has-flag": {
       "dev": true
     },
     "nanoid": {
-      "version": "3.1.25",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz",
-      "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==",
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz",
+      "integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==",
       "dev": true
     },
     "natural-compare": {
       "dev": true
     },
     "nise": {
-      "version": "5.1.0",
-      "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.0.tgz",
-      "integrity": "sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==",
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.1.tgz",
+      "integrity": "sha512-yr5kW2THW1AkxVmCnKEh4nbYkJdB3I7LUkiUgOvEkOp414mc2UMaHMA7pjq1nYowhdoJZGwEKGaQVbxfpWj10A==",
       "dev": true,
       "requires": {
-        "@sinonjs/commons": "^1.7.0",
-        "@sinonjs/fake-timers": "^7.0.4",
+        "@sinonjs/commons": "^1.8.3",
+        "@sinonjs/fake-timers": ">=5",
         "@sinonjs/text-encoding": "^0.7.1",
         "just-extend": "^4.0.2",
         "path-to-regexp": "^1.7.0"
-      },
-      "dependencies": {
-        "@sinonjs/fake-timers": {
-          "version": "7.1.2",
-          "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz",
-          "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==",
-          "dev": true,
-          "requires": {
-            "@sinonjs/commons": "^1.7.0"
-          }
-        }
       }
     },
     "node-preload": {
       "dev": true
     },
     "sinon": {
-      "version": "12.0.1",
-      "resolved": "https://registry.npmjs.org/sinon/-/sinon-12.0.1.tgz",
-      "integrity": "sha512-iGu29Xhym33ydkAT+aNQFBINakjq69kKO6ByPvTsm3yyIACfyQttRTP03aBP/I8GfhFmLzrnKwNNkr0ORb1udg==",
+      "version": "13.0.1",
+      "resolved": "https://registry.npmjs.org/sinon/-/sinon-13.0.1.tgz",
+      "integrity": "sha512-8yx2wIvkBjIq/MGY1D9h1LMraYW+z1X0mb648KZnKSdvLasvDu7maa0dFaNYdTDczFgbjNw2tOmWdTk9saVfwQ==",
       "dev": true,
       "requires": {
         "@sinonjs/commons": "^1.8.3",
-        "@sinonjs/fake-timers": "^8.1.0",
-        "@sinonjs/samsam": "^6.0.2",
+        "@sinonjs/fake-timers": "^9.0.0",
+        "@sinonjs/samsam": "^6.1.1",
         "diff": "^5.0.0",
-        "nise": "^5.1.0",
+        "nise": "^5.1.1",
         "supports-color": "^7.2.0"
       },
       "dependencies": {
       "dev": true
     },
     "workerpool": {
-      "version": "6.1.5",
-      "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz",
-      "integrity": "sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==",
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz",
+      "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==",
       "dev": true
     },
     "wrap-ansi": {
index d12a1af65a26647b648deb676dee45e7c5d1a6cf..d1b8ce5106a67022f487bc83a3919700be8d2595 100644 (file)
   "license": "ISC",
   "dependencies": {
     "@squeep/web-linking": "git+https://git.squeep.com/squeep-web-linking/#v1.0.3",
-    "axios": "^0.25.0",
+    "axios": "^0.26.0",
     "iconv": "^3.0.1",
     "microformats-parser": "^1.4.1"
   },
   "devDependencies": {
-    "eslint": "^8.7.0",
+    "eslint": "^8.9.0",
     "eslint-plugin-node": "^11.1.0",
     "eslint-plugin-security": "^1.4.0",
-    "eslint-plugin-sonarjs": "^0.11.0",
-    "mocha": "^9.1.4",
+    "eslint-plugin-sonarjs": "^0.12.0",
+    "mocha": "^9.2.0",
     "nyc": "^15.1.0",
     "pre-commit": "^1.2.2",
-    "sinon": "^12.0.1"
+    "sinon": "^13.0.1"
   }
 }
index 9a6056db723093696e4e4cc7f72561c3c3b9b54a..5c41469db57a763c3fc202cb769c77b5566e1e3b 100644 (file)
@@ -477,6 +477,45 @@ describe('Communication', function () {
     });
   }); // fetchMicroformat
 
+  describe('fetchJSON', function () {
+    let expected, response, result, urlObj;
+    beforeEach(function () {
+      expected = undefined;
+      result = undefined;
+      urlObj = new URL('https://thuza.ratfeathers.com/');
+      response = {
+        headers: Object.assign({}, testData.linkHeaders),
+        data: testData.hCardHtml,
+      };
+    });
+    it('covers', async function () {
+      communication.axios.resolves(response);
+      expected = { foo: 'bar', baz: 123 };
+      response.data = JSON.stringify(expected);
+
+      result = await communication.fetchJSON(urlObj);
+      assert.deepStrictEqual(result, expected);
+    });
+    it('covers axios error', async function () {
+      communication.axios.rejects(new Error('blah'));
+      expected = undefined;
+
+      result = await communication.fetchJSON(urlObj);
+
+      assert.deepStrictEqual(result, expected);
+    });
+    it('covers non-parsable content', async function () {
+      response.data = 'some bare text';
+      response.headers = {};
+      communication.axios.resolves(response);
+      expected = undefined;
+
+      result = await communication.fetchJSON(urlObj);
+
+      assert.deepStrictEqual(result, expected);
+    });
+  }); // fetchJSON
+
   describe('fetchClientIdentifier', function () {
     let expected, response, result, urlObj;
     beforeEach(function () {
@@ -565,43 +604,141 @@ describe('Communication', function () {
         headers: {},
         data: testData.hCardHtml,
       };
+      sinon.stub(communication, 'fetchJSON');
+    });
+    describe('legacy without indieauth-metadata', function () {
+      it('covers', async function () {
+        communication.axios.resolves(response);
+        expected = {
+          name: 'Thuza',
+          photo: 'https://thuza.ratfeathers.com/image.png',
+          url: 'https://thuza.ratfeathers.com/',
+          email: undefined,
+          authorizationEndpoint: 'https://ia.squeep.com/auth',
+          tokenEndpoint: 'https://ia.squeep.com/token',
+          metadata: {
+            authorizationEndpoint: 'https://ia.squeep.com/auth',
+            tokenEndpoint: 'https://ia.squeep.com/token',  
+          },
+        };
+        result = await communication.fetchProfile(urlObj);
+        assert.deepStrictEqual(result, expected);
+      });
+      it('covers multiple hCards', async function () {
+        response.data = testData.multiMF2Html;
+        communication.axios.resolves(response);
+        expected = {
+          email: undefined,
+          name: 'Thuza',
+          photo: 'https://thuza.ratfeathers.com/image.png',
+          url: 'https://thuza.ratfeathers.com/',
+          authorizationEndpoint: 'https://ia.squeep.com/auth',
+          tokenEndpoint: 'https://ia.squeep.com/token',
+          metadata: {
+            authorizationEndpoint: 'https://ia.squeep.com/auth',
+            tokenEndpoint: 'https://ia.squeep.com/token',  
+          },
+        };
+        result = await communication.fetchProfile(urlObj);
+        assert.deepStrictEqual(result, expected);
+      });
+      it('covers failed fetch', async function () {
+        communication.axios.rejects();
+        expected = {
+          email: undefined,
+          name: undefined,
+          photo: undefined,
+          url: undefined,
+          metadata: {},
+        };
+        result = await communication.fetchProfile(urlObj);
+        assert.deepStrictEqual(result, expected);
+      });
     });
     it('covers', async function () {
+      response.data = testData.hCardMetadataHtml;
       communication.axios.resolves(response);
+      communication.fetchJSON.resolves({
+        'issuer': 'https://ia.squeep.com/',
+        'authorization_endpoint': 'https://ia.squeep.com/auth',
+        'token_endpoint': 'https://ia.squeep.com/token',
+        'introspection_endpoint': 'https://ia.squeep.com/introspect',
+        'introspection_endpoint_auth_methods_supported': [ '' ],
+        'revocation_endpoint': 'https://ia.squeep.com/revoke',
+        'revocation_endpoint_auth_methods_supported': [ 'none' ],
+        'scopes_supported': [ 'profile', 'email' ],
+        'service_documentation': 'https://indieauth.spec.indieweb.org/',
+        'code_challenge_methods_supported': [ 'S256', 'SHA256' ],
+        'authorization_response_iss_parameter_supported': true,
+        'userinfo_endpoint': 'https://ia.squeep.com/userinfo',
+      });
       expected = {
         name: 'Thuza',
         photo: 'https://thuza.ratfeathers.com/image.png',
         url: 'https://thuza.ratfeathers.com/',
         email: undefined,
+        metadata: {
+          authorizationEndpoint: 'https://ia.squeep.com/auth',
+          tokenEndpoint: 'https://ia.squeep.com/token',
+          issuer: 'https://ia.squeep.com/',
+          introspectionEndpoint: 'https://ia.squeep.com/introspect',
+          introspectionEndpointAuthMethodsSupported: [ '' ],
+          revocationEndpoint: 'https://ia.squeep.com/revoke',
+          revocationEndpointAuthMethodsSupported: [ 'none' ],
+          scopesSupported: [ 'profile', 'email' ],
+          serviceDocumentation: 'https://indieauth.spec.indieweb.org/',
+          codeChallengeMethodsSupported: [ 'S256', 'SHA256' ],
+          authorizationResponseIssParameterSupported: true,
+          userinfoEndpoint: 'https://ia.squeep.com/userinfo',
+        },
         authorizationEndpoint: 'https://ia.squeep.com/auth',
         tokenEndpoint: 'https://ia.squeep.com/token',
+        indieauthMetadata: 'https://ia.squeep.com/meta',
       };
+
       result = await communication.fetchProfile(urlObj);
+
       assert.deepStrictEqual(result, expected);
     });
-    it('covers multiple hCards', async function () {
-      response.data = testData.multiMF2Html;
+    it('covers metadata missing fields', async function () {
+      response.data = testData.hCardMetadataHtml;
       communication.axios.resolves(response);
+      communication.fetchJSON.resolves({
+        'issuer': 'https://ia.squeep.com/',
+      });
       expected = {
-        email: undefined,
         name: 'Thuza',
         photo: 'https://thuza.ratfeathers.com/image.png',
         url: 'https://thuza.ratfeathers.com/',
-        authorizationEndpoint: 'https://ia.squeep.com/auth',
-        tokenEndpoint: 'https://ia.squeep.com/token',
+        email: undefined,
+        metadata: {
+          issuer: 'https://ia.squeep.com/',
+        },
+        indieauthMetadata: 'https://ia.squeep.com/meta',
       };
+
       result = await communication.fetchProfile(urlObj);
+
       assert.deepStrictEqual(result, expected);
     });
-    it('covers failed fetch', async function () {
-      communication.axios.rejects();
+    it('covers metadata response failure', async function () {
+      const jsonError = new Error('oh no');
+      response.data = testData.hCardMetadataHtml;
+      communication.axios
+        .onCall(0).resolves(response)
+        .onCall(1).rejects(jsonError);
+      communication.fetchJSON.restore();
       expected = {
+        name: 'Thuza',
+        photo: 'https://thuza.ratfeathers.com/image.png',
+        url: 'https://thuza.ratfeathers.com/',
         email: undefined,
-        name: undefined,
-        photo: undefined,
-        url: undefined,
+        metadata: {},
+        indieauthMetadata: 'https://ia.squeep.com/meta',
       };
+
       result = await communication.fetchProfile(urlObj);
+
       assert.deepStrictEqual(result, expected);
     });
   }); // fetchProfile
@@ -617,7 +754,7 @@ describe('Communication', function () {
     });
     it('covers', async function () {
       communication.axios.resolves({
-        data: '{"me":"https://profile.example.com/"}'
+        data: '{"me":"https://profile.example.com/"}',
       });
       expected = {
         me: 'https://profile.example.com/',
index a78e718e621fb055074e876cdd1e031b67afa812..409e5eb0942c89312e915800be5b84e826657fef 100644 (file)
@@ -64,5 +64,25 @@ module.exports = {
       </section>
     </main>
   </body>
+</html>`,
+  hCardMetadataHtml: `<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta name="viewport" content="width=device-width,initial-scale=1">
+    <title>Thuza Exists</title>
+    <link rel="indieauth-metadata" href="https://ia.squeep.com/meta">
+    <link rel="canonical" href="https://thuza.ratfeathers.com/">
+  </head>
+  <body>
+    <main>
+      <section>
+      Nothing to share yet, only a profile.
+      </section>
+      <section hidden class="h-card p-author">
+        <img class="u-photo" src="image.png">
+        <a class="p-name u-url" rel="author me" href="">Thuza</a>
+      </section>
+    </main>
+  </body>
 </html>`,
   };