feat: request proxying implemented (#74)

* request proxying implemented

* fix: error handling implemented

* fix: fixes after review
This commit is contained in:
Alex Suvorov
2022-05-20 19:02:09 +02:00
committed by GitHub
parent 367a6f188f
commit a4c73ee062
16 changed files with 375 additions and 44 deletions

6
.prettierrc.json Normal file
View File

@@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": false
}

4
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
}

View File

@@ -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",

View File

@@ -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() {

View File

@@ -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();

View File

@@ -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<void> {
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<void> {
const body = {
signerAddress: signedData.signerAddress,
liteSignature: signedData.liteSignature,
provider: providerAddress,
...signedData.pricePackage, // unpacking prices and timestamp
};
providerAddress: string
): Promise<void> {
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);
}
}

View File

@@ -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",

View File

@@ -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);
}

9
src/routes/home.route.ts Normal file
View File

@@ -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 ;)"
);
});
}

12
src/routes/index.ts Normal file
View File

@@ -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);
}

9
src/utils/base64.ts Normal file
View File

@@ -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");
}

View File

@@ -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);
}
}

19
test/routes/_helpers.ts Normal file
View File

@@ -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;
}

View File

@@ -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<typeof axios>;
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);
});
});

View File

@@ -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 ;)");
});
});

View File

@@ -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"