diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..f0eb61e --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": true, + "singleQuote": false +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9bf4d12 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true +} diff --git a/package.json b/package.json index d236cef..e878cb2 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "@types/node": "^14.14.31", "@types/promise-timeout": "^1.3.0", "@types/prompts": "^2.0.14", + "@types/supertest": "^2.0.12", "@types/uuid": "^8.3.0", "@types/yargs": "^16.0.0", "ar-gql": "^0.0.6", @@ -82,6 +83,7 @@ "plotly": "^1.0.6", "prompts": "^2.4.2", "replace-in-file": "^6.3.2", + "supertest": "^6.2.3", "ts-jest": "^27.0.3", "ts-node": "10.0.0", "typed-binary-json": "^1.17.4", diff --git a/src/ExpressAppRunner.ts b/src/ExpressAppRunner.ts index 2f942be..7b039b6 100644 --- a/src/ExpressAppRunner.ts +++ b/src/ExpressAppRunner.ts @@ -1,5 +1,7 @@ import { Consola } from "consola"; -import exprress from "express"; +import express from "express"; +import { setExpressRoutes } from "./routes/index"; +import { NodeConfig } from "./types"; const logger = require("./utils/logger")("express") as Consola; @@ -12,14 +14,11 @@ const PORT = 8080; // This class will be extended in future and will be used for // communication between nodes export class ExpressAppRunner { - private app: exprress.Application; + private app: express.Application; - constructor() { - this.app = exprress(); - - this.app.get("/", (_req, res) => { - res.send("Hello App Runner. My name is RedStone node and I am doing good ;)"); - }); + constructor(private nodeConfig: NodeConfig) { + this.app = express(); + setExpressRoutes(this.app, this.nodeConfig); } run() { diff --git a/src/NodeRunner.ts b/src/NodeRunner.ts index c3007a0..0c3615c 100644 --- a/src/NodeRunner.ts +++ b/src/NodeRunner.ts @@ -85,7 +85,7 @@ export default class NodeRunner { // Running a simple web server // It should be called as early as possible // Otherwise App Runner crashes ¯\_(ツ)_/¯ - new ExpressAppRunner().run(); + new ExpressAppRunner(nodeConfig).run(); const arweave = new ArweaveProxy(jwk); const providerAddress = await arweave.getAddress(); diff --git a/src/broadcasters/http/HttpBroadcaster.ts b/src/broadcasters/http/HttpBroadcaster.ts index 2f55772..810b11d 100644 --- a/src/broadcasters/http/HttpBroadcaster.ts +++ b/src/broadcasters/http/HttpBroadcaster.ts @@ -3,20 +3,26 @@ import mode from "../../../mode"; import { Broadcaster } from "../Broadcaster"; import { PriceDataSigned, SignedPricePackage } from "../../types"; import { Consola } from "consola"; +import { stringifyError } from "../../utils/error-stringifier"; const logger = require("../../utils/logger")("HttpBroadcaster") as Consola; // TODO: add timeout to broadcasting export class HttpBroadcaster implements Broadcaster { - constructor(private readonly broadcasterURLs: string[] = [mode.broadcasterUrl]) {} + constructor( + private readonly broadcasterURLs: string[] = [mode.broadcasterUrl] + ) {} async broadcast(prices: PriceDataSigned[]): Promise { - const promises = this.broadcasterURLs.map(url => { + const promises = this.broadcasterURLs.map((url) => { logger.info(`Posting prices to ${url}`); - return axios.post(url + '/prices', prices) + return axios + .post(url + "/prices", prices) .then(() => logger.info(`Broadcasting to ${url} completed`)) - .catch(e => logger.error(`Broadcasting to ${url} failed: ${errToString(e)}`)); + .catch((e) => + logger.error(`Broadcasting to ${url} failed: ${stringifyError(e)}`) + ); }); await Promise.allSettled(promises); @@ -24,31 +30,27 @@ export class HttpBroadcaster implements Broadcaster { async broadcastPricePackage( signedData: SignedPricePackage, - providerAddress: string): Promise { - const body = { - signerAddress: signedData.signerAddress, - liteSignature: signedData.liteSignature, - provider: providerAddress, - ...signedData.pricePackage, // unpacking prices and timestamp - }; + providerAddress: string + ): Promise { + const body = { + signerAddress: signedData.signerAddress, + liteSignature: signedData.liteSignature, + provider: providerAddress, + ...signedData.pricePackage, // unpacking prices and timestamp + }; - const promises = this.broadcasterURLs.map(url => { - logger.info(`Posting pacakages to ${url}`); - return axios.post(url + '/packages', body) - .then(() => logger.info(`Broadcasting package to ${url} completed`)) - .catch(e => logger.error(`Broadcasting package to ${url} failed: ${errToString(e)}`)); - }); + const promises = this.broadcasterURLs.map((url) => { + logger.info(`Posting pacakages to ${url}`); + return axios + .post(url + "/packages", body) + .then(() => logger.info(`Broadcasting package to ${url} completed`)) + .catch((e) => + logger.error( + `Broadcasting package to ${url} failed: ${stringifyError(e)}` + ) + ); + }); - await Promise.allSettled(promises); - } -} - -// TODO: maybe move this function to a separate module -function errToString(e: any): string { - const responseData = e?.response?.data; - if (responseData) { - return JSON.stringify(responseData); - } else { - return e.toString(); + await Promise.allSettled(promises); } } diff --git a/src/config/nodes.json b/src/config/nodes.json index 1f0ebff..dd1a292 100644 --- a/src/config/nodes.json +++ b/src/config/nodes.json @@ -53,6 +53,12 @@ "evmAddress": "0xdBcC2C6c892C8d3e3Fe4D325fEc810B7376A5Ed6", "ecdsaPublicKey": "0x04feeab427d82702f95c9d1727e47c823c5841ea8b0864607ecb637ff4377604b8213dfd06628d01e624c30dfb20cfa0296be7c1481ce994adb6424d1a391493cf" }, + "redstone-custom-urls-on-demand-1": { + "address": "nrwt7Pwyrt6l6nSTAUwBZW29lx_17-QKmKIsHlXsTPI", + "publicKey": "9C8bFkHr4-rM6UpmQ0bNudZpNW5WB6L1G3Y09jl5IdQ5-vbU_QWxjHhLotmxvU8y9NqQiYSnRAdNk4-uBvmKeRw2uzzCeeE-lUZRJZQnFEy-n-YINXm5zwyUjdKQc4_Oq2rm6F6CSHSsJ8KU0tF5Q04Kv5x3jy2avBORyNDPNorbJR4BKhghG5YoASgcNCQWR_1-T9alrlnUg2LFUQoWyznbZCpDJq3zWNLFzgwY_lsTrTZ5Flm5rsEe_vUl_nrtIESV5Xc6prmNzmxovXja_5b21J28r0edozk_Loa1AJ1FcatvWprjE1umzM5DPFt5Ww4zXguHa1HMJYktccaOxHv0Sqc-k_midyzKo4MSlFaNZIu05uuSAByhw7pbcexLe-yr3vMu0JzVRC3XyLFYgHrXp3j3pcwyax5WdbqJ5Y_vcY6AC7Gvw3TXW26EQuyC6YdUohkTSwa-lCOYQ-iGv0xB1DxFzGmHSNnTTP6_PVBi_Ft7ngZiq51juBb6_32QXW84UeMTm9T5d-62hN4pBtwHHq6NgOgKXXKM2qKEvqfbiXtzfphFYxz4hiULin_LJKldbtvsKIC1igX7DGOHpN3sFrT3HKtmIMoFlUglySsS3qo-s2m63bOS-VfhFVbI_nKBQWyQtPpczHAXVk03DJaXzdPy60EgELR-f_U29_M", + "evmAddress": "0x63b3Cc527bFD6e060EB65d4e902667Ae19aEcEC2", + "ecdsaPublicKey": "0x04fb66f19666c62e0225a53d4b75e6bb74e4512d43936affb64d78fc498618b49b961047d91ed3241b7a03c1044df3fff9fa77a8ec768c776b7a24a3d3880dbace" + }, "redstone-twaps-1": { "address": "aw9F_2R2ogYPnM66TDsW1qtiiRflRcgZQG6OLySOSZE", "publicKey": "sa1_cjxXdrIZgZaxY_L_UkZVf7eL1Q9WTTFrvc9FuRES1ZXwZxXO1QTspji_KboxN54Z8-qFAFXqlun-dgYgGEoLOv57RgWchinlmMHb0QVp2WREe71cSOdioaYfmBhW7eQS8YbvrnFB8cUumdZMOI8UJ6qsBrWOFSDuhPXTdUd2hNZ7mQ9PQR2MMPglZAJnwl36mAb-kaa0cK5s6raeR8_MT0aSSPHQrwyIuM4kOBMvLGEH9XE4LVuNB581Y3h0gXEE_Os7rlhl8BpowkjZM-ZIKK-hUuco9VkjfooQC5CgBHAwivm1--PlIpNa7vtk48cEOkvNoEd0ixN80wfQ2YNgpNFIOV6InnRYyrdPuHBRqE9pCQE48e8VWSxlpgjCiT-bveww42RhRsw_cjVBFHHwMljNQ4bNzjPZLvAaADvSF_ViV4EBpCiqrgj71eyTb0xpUMDSNP7Ae3HcUYPEVC9-n3GjJCCz2akc2zBRxS3zVl13bfApg9JCKzS349Dyaeq9bW9f46CN2U87zLDjUUTJFu3904UumkePlUQjAyMO5YIooChmdoujNla3e_EN6j-SwwjnM3LjDJwNCESETZUrkgZ0Arj6qbENCuEbQLDYqwT7pCJMhXIepolfbloLNnTPTbdCpzyh9pmXwAHokF3hxwSdAzBH11vDSp6ZYvc", diff --git a/src/routes/custom-url-requests.route.ts b/src/routes/custom-url-requests.route.ts new file mode 100644 index 0000000..474fff8 --- /dev/null +++ b/src/routes/custom-url-requests.route.ts @@ -0,0 +1,91 @@ +import express from "express"; +import { Consola } from "consola"; +import { ethers } from "ethers"; +import axios from "axios"; +import jp from "jsonpath"; + +import { fromBase64 } from "../utils/base64"; +import EvmPriceSigner from "../signers/EvmPriceSigner"; +import { NodeConfig } from "../types"; +import { stringifyError } from "../utils/error-stringifier"; + +const EVM_CHAIN_ID = 1; +const QUERY_PARAM_NAME = "custom-url-request-config-base64"; +const DEFAULT_TIMEOUT_MILLISECONDS = 10000; +const EVM_SIGNER_VERSION = "0.4"; +const logger = require("../utils/logger")( + "custom-url-requests-route" +) as Consola; +const evmSigner = new EvmPriceSigner(EVM_SIGNER_VERSION, EVM_CHAIN_ID); + +export default function (app: express.Application, nodeConfig: NodeConfig) { + app.get("/custom-url-requests", async (req, res) => { + try { + // Parsing request details + const customRequestConfig = parseCustomUrlDetails( + req.query[QUERY_PARAM_NAME] as string + ); + const { url, jsonpath } = customRequestConfig; + + // Sending the request + logger.info(`Fetching data from custom url: ${url}`); + const response = await axios.get(url, { + timeout: DEFAULT_TIMEOUT_MILLISECONDS, + }); + const fetchedData = response.data; + logger.info( + `Fetched data from url: ${url}: ${JSON.stringify(fetchedData)}` + ); + + // Extracting value + logger.info(`Extracting data using jsonpath: ${jsonpath}`); + const extractedValueArr = jp.query(fetchedData, jsonpath); + if (extractedValueArr.length !== 1) { + throw new Error(`Extracted value must be a single number`); + } + const extractedValue = extractedValueArr[0]; + if (isNaN(extractedValue)) { + throw new Error(`Extracted value is not a number: ${extractedValue}`); + } + + // Preparing a signed data package + const timestamp = Date.now(); + const symbol = getSymbol({ url, jsonpath }); + const dataPackage = { + timestamp, + prices: [{ symbol, value: extractedValue }], + }; + const signedPackage = evmSigner.signPricePackage( + dataPackage, + nodeConfig.credentials.ethereumPrivateKey + ); + + // Sending response + return res.json({ + signerAddress: signedPackage.signerAddress, + liteSignature: signedPackage.liteSignature, + prices: dataPackage.prices, + customRequestConfig, + timestamp, + }); + } catch (e) { + const errText = stringifyError(e); + // TODO: improve error catching later: + // differentiate types of errors and + // use appropriate HTTP error codes + res.status(400).json({ + err: errText, + }); + } + }); +} + +function parseCustomUrlDetails(customRequestParamBase64: string) { + const stringifiedConfig = fromBase64(customRequestParamBase64); + return JSON.parse(stringifiedConfig); +} + +function getSymbol(customRequestConfig: { url: string; jsonpath: string }) { + const { url, jsonpath } = customRequestConfig; + return ethers.utils.id(`${jsonpath}---${url}`).slice(0, 18); +} diff --git a/src/routes/home.route.ts b/src/routes/home.route.ts new file mode 100644 index 0000000..b5568d8 --- /dev/null +++ b/src/routes/home.route.ts @@ -0,0 +1,9 @@ +import express from "express"; + +export default function (app: express.Application) { + app.get("/", (_req, res) => { + res.send( + "Hello App Runner. My name is RedStone node and I am doing good ;)" + ); + }); +} diff --git a/src/routes/index.ts b/src/routes/index.ts new file mode 100644 index 0000000..1f0a85e --- /dev/null +++ b/src/routes/index.ts @@ -0,0 +1,12 @@ +import express from "express"; +import { NodeConfig } from "../types"; +import setCustomUrlRequestsRoute from "./custom-url-requests.route"; +import setHomeRoute from "./home.route"; + +export function setExpressRoutes( + app: express.Application, + nodeConfig: NodeConfig +) { + setCustomUrlRequestsRoute(app, nodeConfig); + setHomeRoute(app); +} diff --git a/src/utils/base64.ts b/src/utils/base64.ts new file mode 100644 index 0000000..895746c --- /dev/null +++ b/src/utils/base64.ts @@ -0,0 +1,9 @@ +export function fromBase64(base64Str: string): string { + const buff = Buffer.from(base64Str, "base64"); + return buff.toString("utf-8"); +} + +export function toBase64(str: string): string { + const buff = Buffer.from(str, "utf-8"); + return buff.toString("base64"); +} diff --git a/src/utils/error-stringifier.ts b/src/utils/error-stringifier.ts new file mode 100644 index 0000000..9919764 --- /dev/null +++ b/src/utils/error-stringifier.ts @@ -0,0 +1,9 @@ +export function stringifyError(e: any) { + if (e.response) { + return JSON.stringify(e.response.data) + " | " + e.stack; + } else if (e.toJSON) { + return JSON.stringify(e.toJSON()); + } else { + return e.stack || String(e); + } +} diff --git a/test/routes/_helpers.ts b/test/routes/_helpers.ts new file mode 100644 index 0000000..c8775c8 --- /dev/null +++ b/test/routes/_helpers.ts @@ -0,0 +1,19 @@ +import express from "express"; +import { setExpressRoutes } from "../../src/routes/index"; + +const MOCK_NODE_CONFIG = { + arweaveKeysFile: "", + credentials: { + ethereumPrivateKey: "0x1111111111111111111111111111111111111111111111111111111111111111" + }, + addEvmSignature: true, + manifestFile: "", + minimumArBalance: 0.2, + enableStreamrBroadcaster: false, +}; + +export function getApp() { + const app = express(); + setExpressRoutes(app, MOCK_NODE_CONFIG); + return app; +} diff --git a/test/routes/custom-url-requests.route.spec.ts b/test/routes/custom-url-requests.route.spec.ts new file mode 100644 index 0000000..4c3e0f0 --- /dev/null +++ b/test/routes/custom-url-requests.route.spec.ts @@ -0,0 +1,71 @@ +import request from "supertest"; +import axios from "axios"; +import { toBase64 } from "../../src/utils/base64"; +import { getApp } from "./_helpers"; + +const app = getApp(); + +// Mock axios response +const exampleResponse = { + A: { + B: { + C: 42, + }, + }, +}; +jest.mock("axios"); +const mockedAxios = axios as jest.Mocked; +mockedAxios.get.mockResolvedValue({ data: exampleResponse }); + +// Mock current timestamp +Date.now = jest.fn(() => 1652662184000); + +describe("Custom URL requests route", () => { + test("Should send correct response ", async () => { + const customUrlRequestConfigBase64 = toBase64( + JSON.stringify({ + url: "https://example-custom-data-source.com/hehe", + jsonpath: "$.A.B.C", + }) + ); + const response = await request(app) + .get("/custom-url-requests") + .query({ + "custom-url-request-config-base64": customUrlRequestConfigBase64, + }) + .expect(200); + + expect(response.body).toEqual({ + signerAddress: "0x19E7E376E7C213B7E7e7e46cc70A5dD086DAff2A", + liteSignature: + "0x6a4fab1950cf3bfd0ff0df5ef50b87d1a39cd451476081d7ef50179562aacd940ac81ddbde946dcf4c45f4cf41eebe876f0e82cd343c9a0799943c95bbad1ee51c", + prices: [{ symbol: "0x8edd634f1bbd8320", value: 42 }], + customRequestConfig: { + url: "https://example-custom-data-source.com/hehe", + jsonpath: "$.A.B.C", + }, + timestamp: 1652662184000, + }); + }); + + test("Should handle invalid values correctly ", async () => { + mockedAxios.get.mockResolvedValue({ data: { bad: "value" } }); + const customUrlRequestConfigBase64 = toBase64( + JSON.stringify({ + url: "https://example-custom-data-source.com/hehe", + jsonpath: "$.A.B.C", + }) + ); + await request(app) + .get("/custom-url-requests") + .query({ + "custom-url-request-config-base64": customUrlRequestConfigBase64, + }) + .expect(400); + }); + + test("Should handle invalid request params ", async () => { + mockedAxios.get.mockResolvedValue({ data: { bad: "value" } }); + await request(app).get("/custom-url-requests").expect(400); + }); +}); diff --git a/test/routes/home.route.spec.ts b/test/routes/home.route.spec.ts new file mode 100644 index 0000000..d4b71c6 --- /dev/null +++ b/test/routes/home.route.spec.ts @@ -0,0 +1,14 @@ +import request from "supertest"; +import { getApp } from "./_helpers"; + +const app = getApp(); + +describe("Custom url requests route", () => { + test("Should send correct response ", async () => { + const response = await request(app) + .get("/") + .expect(200); + + expect(response.text).toBe("Hello App Runner. My name is RedStone node and I am doing good ;)"); + }); +}); diff --git a/yarn.lock b/yarn.lock index dfe7739..13209f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1327,6 +1327,11 @@ resolved "https://registry.yarnpkg.com/@types/content-disposition/-/content-disposition-0.5.5.tgz#650820e95de346e1f84e30667d168c8fd25aa6e3" integrity sha512-v6LCdKfK6BwcqMo+wYW05rLS12S0ZO0Fl4w1h4aaZMD7bqT3gVUns6FvLJKGZHQmYn3SX55JWGpziwJRwVgutA== +"@types/cookiejar@*": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@types/cookiejar/-/cookiejar-2.1.2.tgz#66ad9331f63fe8a3d3d9d8c6e3906dd10f6446e8" + integrity sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog== + "@types/cookies@*": version "0.7.7" resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.7.7.tgz#7a92453d1d16389c05a5301eef566f34946cfd81" @@ -1583,6 +1588,21 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/superagent@*": + version "4.1.15" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-4.1.15.tgz#63297de457eba5e2bc502a7609426c4cceab434a" + integrity sha512-mu/N4uvfDN2zVQQ5AYJI/g4qxn2bHB6521t1UuH09ShNWjebTqN0ZFuYK9uYjcgmI0dTQEs+Owi1EO6U0OkOZQ== + dependencies: + "@types/cookiejar" "*" + "@types/node" "*" + +"@types/supertest@^2.0.12": + version "2.0.12" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.12.tgz#ddb4a0568597c9aadff8dbec5b2e8fddbe8692fc" + integrity sha512-X3HPWTwXRerBZS7Mo1k6vMVR1Z6zmJcDVn5O/31whe0tnjE4te6ZJSJGq1RiqHPjzPdMTfjCFogDJmwng9xHaQ== + dependencies: + "@types/superagent" "*" + "@types/through@*": version "0.0.30" resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895" @@ -2088,6 +2108,11 @@ arweave@1.10.23, arweave@^1.10.13, arweave@^1.10.15, arweave@^1.10.16, arweave@^ bignumber.js "^9.0.1" util "^0.12.4" +asap@^2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= + asn1.js@^5.4.1: version "5.4.1" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" @@ -2909,7 +2934,7 @@ cookie@0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== -cookiejar@^2.1.2: +cookiejar@^2.1.2, cookiejar@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.3.tgz#fc7a6216e408e74414b90230050842dacda75acc" integrity sha512-JxbCBUdrfr6AQjOXrxoTvAMJO4HBTUIlBzslcJPAz+/KT8yk53fXun51u+RenNYvad/+Vc2DIz5o9UxlCDymFQ== @@ -3102,7 +3127,7 @@ debug@2.6.9, debug@^2.3.3: dependencies: ms "2.0.0" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3: +debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -3273,6 +3298,14 @@ detect-newline@^3.0.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +dezalgo@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" + integrity sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY= + dependencies: + asap "^2.0.0" + wrappy "1" + diff-sequences@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" @@ -4003,7 +4036,7 @@ fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= -fast-safe-stringify@^2.0.7: +fast-safe-stringify@^2.0.7, fast-safe-stringify@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== @@ -4158,6 +4191,16 @@ formidable@^1.1.1, formidable@^1.2.2: resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168" integrity sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ== +formidable@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.0.1.tgz#4310bc7965d185536f9565184dee74fbb75557ff" + integrity sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ== + dependencies: + dezalgo "1.0.3" + hexoid "1.0.0" + once "1.4.0" + qs "6.9.3" + forwarded@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" @@ -4476,6 +4519,11 @@ heap@^0.2.6: resolved "https://registry.yarnpkg.com/heap/-/heap-0.2.7.tgz#1e6adf711d3f27ce35a81fe3b7bd576c2260a8fc" integrity sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg== +hexoid@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" + integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== + hi-base32@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/hi-base32/-/hi-base32-0.5.1.tgz#1279f2ddae2673219ea5870c2121d2a33132857e" @@ -5995,7 +6043,7 @@ mime@1.6.0: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== -mime@^2.4.6: +mime@^2.4.6, mime@^2.5.0: version "2.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== @@ -6384,7 +6432,7 @@ on-finished@2.4.1, on-finished@^2.3.0: dependencies: ee-first "1.1.1" -once@^1.3.0, once@^1.3.1, once@^1.4.0: +once@1.4.0, once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= @@ -6825,13 +6873,18 @@ pvutils@^1.1.3: resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.3.tgz#f35fc1d27e7cd3dfbd39c0826d173e806a03f5a3" integrity sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ== -qs@6.10.3, qs@^6.10.1, qs@^6.4.0, qs@^6.5.2, qs@^6.9.4: +qs@6.10.3, qs@^6.10.1, qs@^6.10.3, qs@^6.4.0, qs@^6.5.2, qs@^6.9.4: version "6.10.3" resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ== dependencies: side-channel "^1.0.4" +qs@6.9.3: + version "6.9.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e" + integrity sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw== + qs@~6.5.2: version "6.5.3" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" @@ -7244,7 +7297,7 @@ seek-bzip@^1.0.5: dependencies: commander "^2.8.1" -semver@7.x, semver@^7.3.2, semver@^7.3.5: +semver@7.x, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7: version "7.3.7" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== @@ -7763,11 +7816,36 @@ superagent@^6.1.0: readable-stream "^3.6.0" semver "^7.3.2" +superagent@^7.1.3: + version "7.1.3" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-7.1.3.tgz#783ff8330e7c2dad6ad8f0095edc772999273b6b" + integrity sha512-WA6et4nAvgBCS73lJvv1D0ssI5uk5Gh+TGN/kNe+B608EtcVs/yzfl+OLXTzDs7tOBDIpvgh/WUs1K2OK1zTeQ== + dependencies: + component-emitter "^1.3.0" + cookiejar "^2.1.3" + debug "^4.3.4" + fast-safe-stringify "^2.1.1" + form-data "^4.0.0" + formidable "^2.0.1" + methods "^1.1.2" + mime "^2.5.0" + qs "^6.10.3" + readable-stream "^3.6.0" + semver "^7.3.7" + superstruct@^0.14.2: version "0.14.2" resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.14.2.tgz#0dbcdf3d83676588828f1cf5ed35cda02f59025b" integrity sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ== +supertest@^6.2.3: + version "6.2.3" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.2.3.tgz#291b220126e5faa654d12abe1ada3658757c8c67" + integrity sha512-3GSdMYTMItzsSYjnIcljxMVZKPW1J9kYHZY+7yLfD0wpPwww97GeImZC1oOk0S5+wYl2niJwuFusBJqwLqYM3g== + dependencies: + methods "^1.1.2" + superagent "^7.1.3" + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"