diff --git a/oneuptime-acme-http-01/.gitignore b/oneuptime-acme-http-01/.gitignore new file mode 100644 index 0000000000..30bc162798 --- /dev/null +++ b/oneuptime-acme-http-01/.gitignore @@ -0,0 +1 @@ +/node_modules \ No newline at end of file diff --git a/oneuptime-acme-http-01/README.md b/oneuptime-acme-http-01/README.md new file mode 100644 index 0000000000..c7ef1a036e --- /dev/null +++ b/oneuptime-acme-http-01/README.md @@ -0,0 +1,21 @@ +# oneuptime-acme-http-01 Package for Greenlock + +This module handles acme-http-01 challenge and also the api call to OneUptime backend to persist keyAuthorization and token data from acme directory in our mongodb. The stored data will be used by OneUptime to validate all our certificate order/renewal. + +## Install + + npm install oneuptime-acme-http-01 + +## Usage + + // make sure greenlock is already installed + const Greenlock = require('greenlock'); + + Greenlock.create({ + challenges: { + 'http-01': { + module: 'oneuptime-acme-http-01', + }, + }, + // ... + }); diff --git a/oneuptime-acme-http-01/index.js b/oneuptime-acme-http-01/index.js new file mode 100644 index 0000000000..647221a0d1 --- /dev/null +++ b/oneuptime-acme-http-01/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./lib/index.js'); diff --git a/oneuptime-acme-http-01/lib/index.js b/oneuptime-acme-http-01/lib/index.js new file mode 100644 index 0000000000..9992449b64 --- /dev/null +++ b/oneuptime-acme-http-01/lib/index.js @@ -0,0 +1,49 @@ +'use strict'; + +const axios = require('axios'); +const BASE_URL = `${process.env.BACKEND_PROTOCOL}://${process.env.ONEUPTIME_HOST}`; + +module.exports = { + create: function(config) { + return { + // init: function(opts) { + // //request = opts.request; + // return Promise.resolve(null); + // }, + + set: function(data) { + const ch = data.challenge; + + // make api call to backend to store + // keyAuthorization, challengeUrl, and token + const url = `${BASE_URL}/api/ssl/challenge`; + const dataConfig = { + token: ch.token, + keyAuthorization: ch.keyAuthorization, + challengeUrl: ch.challengeUrl, + }; + return axios({ + url, + method: 'post', + data: dataConfig, + }).finally(() => null); // always return null + }, + + get: function(data) { + const ch = data.challenge; + + const url = `${BASE_URL}/api/ssl/challenge/${ch.token}`; + return axios.get(url).then(result => result); + }, + + remove: function(data) { + const ch = data.challenge; + + const url = `${BASE_URL}/api/ssl/challenge/${ch.token}`; + return axios({ url, method: 'delete' }).finally(() => null); // always return null + }, + + options: config, + }; + }, +}; diff --git a/oneuptime-acme-http-01/package-lock.json b/oneuptime-acme-http-01/package-lock.json new file mode 100644 index 0000000000..fbca5c16ce --- /dev/null +++ b/oneuptime-acme-http-01/package-lock.json @@ -0,0 +1,106 @@ +{ + "name": "oneuptime-acme-http-01", + "version": "3.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "axios": "^0.21.1" + }, + "devDependencies": { + "acme-challenge-test": "^3.3.2", + "dotenv": "^8.0.0" + } + }, + "node_modules/@root/request": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@root/request/-/request-1.7.0.tgz", + "integrity": "sha512-lre7XVeEwszgyrayWWb/kRn5fuJfa+n0Nh+rflM9E+EpC28yIYA+FPm/OL1uhzp3TxhQM0HFN4FE2RDIPGlnmg==", + "dev": true + }, + "node_modules/acme-challenge-test": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/acme-challenge-test/-/acme-challenge-test-3.3.2.tgz", + "integrity": "sha512-0AbMcaON20wpI5vzFDAqwcv2VerY4xIlNCqX0w1xEJUIu/EQtQNmkje+rKNuy2TUl2KBMdIaR6YBbJUdaEiC4w==", + "dev": true, + "dependencies": { + "@root/request": "^1.3.11" + } + }, + "node_modules/axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "dependencies": { + "follow-redirects": "^1.10.0" + } + }, + "node_modules/dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz", + "integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + } + }, + "dependencies": { + "@root/request": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@root/request/-/request-1.7.0.tgz", + "integrity": "sha512-lre7XVeEwszgyrayWWb/kRn5fuJfa+n0Nh+rflM9E+EpC28yIYA+FPm/OL1uhzp3TxhQM0HFN4FE2RDIPGlnmg==", + "dev": true + }, + "acme-challenge-test": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/acme-challenge-test/-/acme-challenge-test-3.3.2.tgz", + "integrity": "sha512-0AbMcaON20wpI5vzFDAqwcv2VerY4xIlNCqX0w1xEJUIu/EQtQNmkje+rKNuy2TUl2KBMdIaR6YBbJUdaEiC4w==", + "dev": true, + "requires": { + "@root/request": "^1.3.11" + } + }, + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, + "dotenv": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", + "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==", + "dev": true + }, + "follow-redirects": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz", + "integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==" + } + } +} diff --git a/oneuptime-acme-http-01/package.json b/oneuptime-acme-http-01/package.json new file mode 100644 index 0000000000..0511635afb --- /dev/null +++ b/oneuptime-acme-http-01/package.json @@ -0,0 +1,35 @@ +{ + "name": "oneuptime-acme-http-01", + "version": "3.0.0", + "description": "HTTP Authentication (In-Memory) for Let's Encrypt for Node.js - ACME http-01 challenges for OneUptime", + "main": "index.js", + "files": [ + "*.js", + "lib", + "test.js" + ], + "scripts": { + "test": "node test.js" + }, + "keywords": [ + "standalone", + "memory", + "http-01", + "letsencrypt", + "acme", + "greenlock", + "oneuptime" + ], + "author": "Jude Ojini ", + "contributors": [ + "Jude Ojini " + ], + "license": "MIT", + "devDependencies": { + "acme-challenge-test": "^3.3.2", + "dotenv": "^8.0.0" + }, + "dependencies": { + "axios": "^0.21.1" + } +} diff --git a/oneuptime-acme-http-01/test.js b/oneuptime-acme-http-01/test.js new file mode 100755 index 0000000000..c8009bdd86 --- /dev/null +++ b/oneuptime-acme-http-01/test.js @@ -0,0 +1,26 @@ +#!/usr/bin/env node +'use strict'; + +// See https://git.coolaj86.com/coolaj86/acme-challenge-test.js +const tester = require('acme-challenge-test'); +require('dotenv').config(); + +// Usage: node ./test.js example.com username xxxxxxxxx +const record = process.argv[2] || process.env.RECORD; +const challenger = require('./index.js').create({}); + +// The dry-run tests can pass on, literally, 'example.com' +// but the integration tests require that you have control over the domain + +tester + .testRecord('http-01', record, challenger) + .then(function() { + // eslint-disable-next-line no-console + console.info('PASS', record); + }) + .catch(function(e) { + // eslint-disable-next-line no-console + console.error(e.message); + // eslint-disable-next-line no-console + console.error(e.stack); + }); diff --git a/oneuptime-gl-manager/README.md b/oneuptime-gl-manager/README.md new file mode 100644 index 0000000000..04d0a952fb --- /dev/null +++ b/oneuptime-gl-manager/README.md @@ -0,0 +1,73 @@ +# oneuptime-gl-manager + +Manages SSL Certificate issuance and renewal for [Greenlock](https://git.rootprojects.org/root/greenlock-manager.js) on [OneUptime](https://oneuptime.com) platform. + +Saves global and per-site config to a local File Sytem (current). + +## Install + +```bash +npm install --save oneuptime-gl-manager +``` + +# Usage + +## Initialize the Manager + +```js +Greenlock.create({ + ... + manager: "oneuptime-gl-manager", + configDir: "./greenlock.d", + packageRoot: __dirname + ... +}); +``` + +# Site Management + +By "site" we mean a primary domain and, optionally, secondary domains, to be listed on an ssl certificate, +along with any configuration that is necessary for getting and renewing those certificates. + +## Add a sites - domains and SSL certificates + +```js +greenlock.add({ + subject: 'example.com', + altnames: ['example.com', 'www.example.com'], +}); +``` + +## View site config + +```js +greenlock.get({ + servername: 'www.example.com', + wildname: '*.example.com', +}); +``` + +## Update site config + +```js +greenlock.update({ + subject: 'www.example.com', + challenges: { + 'dns-01': { + module: 'acme-dns-01-ovh', + token: 'xxxx', + }, + }, +}); +``` + +## Remove a site + +To stop automatic renewal of SSL certificates for a particular site. +You to restart renewal you must use `add()`. + +```js +greenlock.remove({ + subject: 'example.com', +}); +``` diff --git a/oneuptime-gl-manager/manager.js b/oneuptime-gl-manager/manager.js new file mode 100644 index 0000000000..aae220fec7 --- /dev/null +++ b/oneuptime-gl-manager/manager.js @@ -0,0 +1,127 @@ +'use strict'; + +const axios = require('axios'); +const BASE_URL = `${process.env.BACKEND_PROTOCOL}://${process.env.ONEUPTIME_HOST}`; + +const Manager = module.exports; +// eslint-disable-next-line no-unused-vars +Manager.create = function(opts) { + const manager = {}; + + // + // REQUIRED (basic issuance) + // + manager.get = async function({ servername }) { + const url = `${BASE_URL}/api/manager/site?servername=${servername}`; + const response = await axios({ + url, + method: 'get', + }); + + return response.data; + }; + + // + // REQUIRED (basic issuance) + // + manager.set = async function(opts) { + const url = `${BASE_URL}/api/manager/site?subject=${opts.subject}`; + const response = await axios({ + url, + method: 'put', + data: opts, + }); + + return response.data; + }; + + // + // Optional (Fully Automatic Renewal) + // + manager.find = async function(opts) { + // { subject, servernames, altnames, renewBefore } + if (opts.subject) { + const url = `${BASE_URL}/api/manager/site?subject=${opts.subject}`; + const response = await axios({ + url, + method: 'get', + }); + if (!response.data || response.data.length === 0) { + return []; + } + + return [response.data]; + } + + if (Array.isArray(opts.servernames) && opts.servernames.length > 0) { + const url = `${BASE_URL}/api/manager/site/servernames`; + const response = await axios({ + url, + method: 'post', + data: opts.servernames, + }); + + return response.data; + } + + // i.e. find certs more than 30 days old as default + opts.issuedBefore = + opts.issuedBefore || Date.now() - 30 * 24 * 60 * 60 * 1000; + // i.e. find certs that will expire in less than 45 days as default + opts.expiresBefore = + opts.expiresBefore || Date.now() + 45 * 24 * 60 * 60 * 1000; + // i.e. find certs that should be renewed within 21 days as default + opts.renewBefore = + opts.renewBefore || Date.now() + 21 * 24 * 60 * 60 * 1000; + + const url = `${BASE_URL}/api/manager/site/opts`; + const response = await axios({ + url, + method: 'post', + data: opts, + }); + + return response.data; + }; + + // + // Optional (Special Remove Functionality) + // The default behavior is to set `deletedAt` + // + manager.remove = async function(opts) { + const url = `${BASE_URL}/api/manager/site?subject=${opts.subject}`; + const response = await axios({ + url, + method: 'delete', + }); + + return response.data; + }; + + // + // Optional (special settings save) + // Implemented here because this module IS the fallback + // This is a setter/getter function + // + manager.defaults = async function(opts) { + if (!opts) { + const url = `${BASE_URL}/api/manager/default`; + const response = await axios({ + url, + method: 'get', + }); + return response.data ? response.data : {}; + } + + const url = `${BASE_URL}/api/manager/default`; + const response = await axios({ + url, + method: 'put', + data: opts, + }); + + return response.data || {}; + }; + + return manager; +}; diff --git a/oneuptime-gl-manager/package-lock.json b/oneuptime-gl-manager/package-lock.json new file mode 100644 index 0000000000..cfbfbac8bb --- /dev/null +++ b/oneuptime-gl-manager/package-lock.json @@ -0,0 +1,57 @@ +{ + "name": "oneuptime-gl-manager", + "version": "3.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "axios": "^0.21.1" + } + }, + "node_modules/axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "dependencies": { + "follow-redirects": "^1.10.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", + "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + } + }, + "dependencies": { + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, + "follow-redirects": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", + "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==" + } + } +} diff --git a/oneuptime-gl-manager/package.json b/oneuptime-gl-manager/package.json new file mode 100644 index 0000000000..7d10c003d0 --- /dev/null +++ b/oneuptime-gl-manager/package.json @@ -0,0 +1,28 @@ +{ + "name": "oneuptime-gl-manager", + "version": "3.0.0", + "description": "FileSytem-based Manager with optional encrypted Cloud backup for Greenlock SSL", + "main": "manager.js", + "files": [ + "*.js" + ], + "scripts": { + "test": "node tests" + }, + "keywords": [ + "greenlock", + "manager", + "cloud", + "fs", + "ssl", + "oneuptime" + ], + "author": "Jude Ojini ", + "contributors": [ + "Jude Ojini " + ], + "license": "MIT", + "dependencies": { + "axios": "^0.21.1" + } +} diff --git a/oneuptime-le-store/.gitignore b/oneuptime-le-store/.gitignore new file mode 100644 index 0000000000..30bc162798 --- /dev/null +++ b/oneuptime-le-store/.gitignore @@ -0,0 +1 @@ +/node_modules \ No newline at end of file diff --git a/oneuptime-le-store/README.md b/oneuptime-le-store/README.md new file mode 100644 index 0000000000..676f5bbf5b --- /dev/null +++ b/oneuptime-le-store/README.md @@ -0,0 +1,19 @@ +# oneuptime-le-store Package for Greenlock + +This module implements a dead-simple, api call to OneUptime backend to store account or certificate details. This allows us to persist our [Let's Encrypt](https://letsencrypt.org/) data in mongo for automated TLS certificate issuance and use. + +## Install + + npm install oneuptime-le-store + +## Usage + + // make sure greenlock is already installed + const Greenlock = require('greenlock'); + + Greenlock.create({ + store: { + module: 'oneuptime-le-store', + }, + // ... + }); diff --git a/oneuptime-le-store/index.js b/oneuptime-le-store/index.js new file mode 100644 index 0000000000..381ca51eff --- /dev/null +++ b/oneuptime-le-store/index.js @@ -0,0 +1,118 @@ +'use strict'; + +const axios = require('axios'); +const BASE_URL = `${process.env.BACKEND_PROTOCOL}://${process.env.ONEUPTIME_HOST}`; + +// make api call to designated endpoints +// to make the necessary updates to the db +module.exports.create = function(config) { + const store = {}; + store.options = config; + + store.accounts = { + setKeypair: function(opts) { + const id = + (opts.account && opts.account.id) || opts.email || 'default'; + + const url = `${BASE_URL}/api/account/store/${id}`; + const data = { + id: id, + privateKeyPem: opts.keypair.privateKeyPem, + privateKeyJwk: opts.keypair.privateKeyJwk, + publickKeyPem: opts.keypair.publickeyPem, + publicKeyJwk: opts.keypair.publicKeyJwk, + key: opts.keypair.key, + }; + return axios({ + url, + method: 'put', + data, + }) + .then(res => res.data) + .finally(() => null); + }, + checkKeypair: function(opts) { + const id = + (opts.account && opts.account.id) || opts.email || 'default'; + + const url = `${BASE_URL}/api/account/store/${id}`; + return axios({ + url, + method: 'get', + }) + .then(res => res.data) + .finally(() => null); + }, + options: config, + }; + + store.certificates = { + setKeypair: function(opts) { + const id = + (opts.certificate && + (opts.certificate.kid || opts.certificate.id)) || + opts.subject; + + const url = `${BASE_URL}/api/certificate/store/${id}`; + const data = { + id: id, + deleted: false, + ...opts.keypair, + }; + return axios({ + url, + method: 'put', + data, + }) + .then(res => res.data) + .finally(() => null); + }, + checkKeypair: function(opts) { + const id = + (opts.certificate && + (opts.certificate.kid || opts.certificate.id)) || + opts.subject; + + const url = `${BASE_URL}/api/certificate/store/${id}`; + return axios({ + url, + method: 'get', + }) + .then(res => res.data) + .finally(() => null); + }, + set: function(opts) { + const id = + (opts.certificate && opts.certificate.id) || opts.subject; + + const url = `${BASE_URL}/api/certificate/store/${id}`; + const data = { + id: id, + deleted: false, + ...opts.pems, + }; + return axios({ + url, + method: 'put', + data, + }) + .then(res => res.data) + .finally(() => null); + }, + check: function(opts) { + const id = + (opts.certificate && opts.certificate.id) || opts.subject; + + const url = `${BASE_URL}/api/certificate/store/${id}`; + return axios({ + url, + method: 'get', + }) + .then(res => res.data) + .finally(() => null); + }, + options: config, + }; + + return store; +}; diff --git a/oneuptime-le-store/package-lock.json b/oneuptime-le-store/package-lock.json new file mode 100644 index 0000000000..3ba433909b --- /dev/null +++ b/oneuptime-le-store/package-lock.json @@ -0,0 +1,57 @@ +{ + "name": "oneuptime-le-store", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "axios": "^0.21.1" + } + }, + "node_modules/axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "dependencies": { + "follow-redirects": "^1.10.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz", + "integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + } + }, + "dependencies": { + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, + "follow-redirects": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz", + "integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==" + } + } +} diff --git a/oneuptime-le-store/package.json b/oneuptime-le-store/package.json new file mode 100644 index 0000000000..ca2fc804c7 --- /dev/null +++ b/oneuptime-le-store/package.json @@ -0,0 +1,22 @@ +{ + "name": "oneuptime-le-store", + "version": "3.0.0", + "description": "Greenlock store module ported for OneUptime", + "main": "index.js", + "keywords": [ + "le-store", + "greenlock", + "letsencrypt", + "mongodb", + "mongo", + "oneuptime" + ], + "author": "Jude Ojini ", + "contributors": [ + "Jude Ojini " + ], + "license": "MIT", + "dependencies": { + "axios": "^0.21.1" + } +}