From 8daa668e5a0485b5f0e2b07203d9fb237d7f761c Mon Sep 17 00:00:00 2001 From: Justin Wind Date: Fri, 18 Feb 2022 11:20:53 -0800 Subject: [PATCH] support indieauth-metadata relation on profiles, per spec 20220212 --- README.md | 13 ++- lib/communication.js | 121 +++++++++++++++++++-- lib/enum.js | 2 +- lib/utility.js | 0 package-lock.json | 182 +++++++++++++++----------------- package.json | 10 +- test/lib/communication.js | 159 ++++++++++++++++++++++++++-- test/test-data/communication.js | 20 ++++ 8 files changed, 384 insertions(+), 123 deletions(-) delete mode 100644 lib/utility.js diff --git a/README.md b/README.md index dee619d..bd005a7 100644 --- 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. diff --git a/lib/communication.js b/lib/communication.js index 51f5b43..2b0c621 100644 --- a/lib/communication.js +++ b/lib/communication.js @@ -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; } diff --git a/lib/enum.js b/lib/enum.js index ef5c0ff..dcec131 100644 --- a/lib/enum.js +++ b/lib/enum.js @@ -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 index e69de29..0000000 diff --git a/package-lock.json b/package-lock.json index 81b5367..049166f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -273,14 +273,14 @@ } }, "@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", @@ -313,9 +313,9 @@ } }, "@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", @@ -409,18 +409,18 @@ } }, "@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", @@ -534,11 +534,11 @@ } }, "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": { @@ -679,9 +679,9 @@ } }, "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", @@ -870,12 +870,12 @@ "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", @@ -883,10 +883,10 @@ "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", @@ -980,15 +980,15 @@ } }, "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", @@ -1013,20 +1013,20 @@ } }, "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": { @@ -1139,15 +1139,15 @@ } }, "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", @@ -1225,9 +1225,9 @@ } }, "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" @@ -1664,32 +1664,32 @@ "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" @@ -1701,18 +1701,21 @@ "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": { @@ -1754,9 +1757,9 @@ "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": { @@ -1766,27 +1769,16 @@ "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": { @@ -2410,16 +2402,16 @@ "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": { @@ -2651,9 +2643,9 @@ "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": { diff --git a/package.json b/package.json index d12a1af..d1b8ce5 100644 --- a/package.json +++ b/package.json @@ -26,18 +26,18 @@ "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" } } diff --git a/test/lib/communication.js b/test/lib/communication.js index 9a6056d..5c41469 100644 --- a/test/lib/communication.js +++ b/test/lib/communication.js @@ -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/', diff --git a/test/test-data/communication.js b/test/test-data/communication.js index a78e718..409e5eb 100644 --- a/test/test-data/communication.js +++ b/test/test-data/communication.js @@ -64,5 +64,25 @@ module.exports = { +`, + hCardMetadataHtml: ` + + + + Thuza Exists + + + + +
+
+ Nothing to share yet, only a profile. +
+ +
+ `, }; -- 2.43.2