From 0f808b4b87eb72942eb098e8c3e4f13ea7699a90 Mon Sep 17 00:00:00 2001 From: Victor Elias Date: Tue, 2 Aug 2022 17:00:56 -0300 Subject: [PATCH] api: Implement resumable uploads for VOD (#1112) * api: Add tus-node-server dep * asset: Implement tus upload to local filesystem * Remove alternate ways of passing uploadToken * Throw an idea for using tus metadata * WIP: Idea on how tus could be configured from VOD OS * [DEV-ONLY] Add tus-test project to test tus development Remember to delete this or make it a proper unit test once it's done. * api: moved TUS into req, added GCSStore, fixed TaskScheduler type, added gcsOptions to schema * removed auto import * update with google storage as s3 * upload.start() * upload.start() * added tests * Make resumable uploads work! * api: Move setupTus logic to asset controller Now app-router only needs to call a func * api/asset: Upload tus files to directUpload folder * api/asset: Disallow users from customizing OS * tus: setup test tus * vod: added tus resumable upload tests * tus: added mock file instead of test file * api/asset: Fix tusEndpoint returned during tests * tus: remove tus-test folder - update tests * tus: fixed tests * fix taskScheduler type * api/asset: Fix tests Was missing the tus-js-client dependency and needed to fix the check on URL format. Co-authored-by: gioelecerati --- packages/api/package.json | 2 + packages/api/src/app-router.ts | 16 +- packages/api/src/controllers/asset.test.ts | 118 ++++++- packages/api/src/controllers/asset.ts | 185 ++++++++-- packages/api/src/store/errors.ts | 8 + packages/api/src/task/scheduler.ts | 17 +- packages/api/src/test-helpers.ts | 10 + packages/api/src/test-server.ts | 2 +- packages/api/src/types/common.d.ts | 1 - packages/api/src/webhooks/cannon.ts | 7 +- yarn.lock | 376 ++++++++++++++++++++- 11 files changed, 681 insertions(+), 61 deletions(-) diff --git a/packages/api/package.json b/packages/api/package.json index 60c716bd17..9dd4bb8ec1 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -100,6 +100,7 @@ "sql-template-strings": "^2.2.2", "string-template": "^1.0.0", "stripe": "^8.93.0", + "tus-node-server": "^0.6.0", "uuid": "^3.3.2", "whatwg-fetch": "^3.4.0", "winston": "^3.2.1", @@ -138,6 +139,7 @@ "pkg": "^5.7.0", "prettier": "^2.0.5", "redoc-cli": "^0.8.3", + "tus-js-client": "^3.0.0-0", "typescript": "^4.3.4", "wrangler": "^0.0.2" }, diff --git a/packages/api/src/app-router.ts b/packages/api/src/app-router.ts index 35463a8f5b..c38c570187 100644 --- a/packages/api/src/app-router.ts +++ b/packages/api/src/app-router.ts @@ -3,7 +3,6 @@ import { Router } from "express"; import promBundle from "express-prom-bundle"; import proxy from "http-proxy-middleware"; import Stripe from "stripe"; - import makeStore from "./store"; import { errorHandler, @@ -20,11 +19,12 @@ import streamProxy from "./controllers/stream-proxy"; import apiProxy from "./controllers/api-proxy"; import { getBroadcasterHandler } from "./controllers/broadcaster"; import WebhookCannon from "./webhooks/cannon"; -import TaskScheduler from "./task/scheduler"; import Queue, { NoopQueue, RabbitQueue } from "./store/queue"; import { CliArgs } from "./parse-cli"; import { regionsGetter } from "./controllers/region"; import { pathJoin } from "./controllers/helpers"; +import taskScheduler from "./task/scheduler"; +import { setupTus, setupTestTus } from "./controllers/asset"; enum OrchestratorSource { hardcoded = "hardcoded", @@ -102,10 +102,7 @@ export default async function makeApp(params: CliArgs) { : new NoopQueue(); // Task Scheduler - const taskScheduler = new TaskScheduler({ - queue, - }); - await taskScheduler.start(); + await taskScheduler.start({ queue }); // Webhooks Cannon const webhookCannon = new WebhookCannon({ @@ -113,7 +110,6 @@ export default async function makeApp(params: CliArgs) { frontendDomain, sendgridTemplateId, sendgridApiKey, - taskScheduler, vodObjectStoreId, supportAddr, verifyUrls: true, @@ -121,6 +117,12 @@ export default async function makeApp(params: CliArgs) { }); await webhookCannon.start(); + if (process.env.NODE_ENV === "test") { + await setupTestTus(); + } else if (vodObjectStoreId) { + await setupTus(vodObjectStoreId); + } + process.on("beforeExit", (code) => { queue.close(); webhookCannon.stop(); diff --git a/packages/api/src/controllers/asset.test.ts b/packages/api/src/controllers/asset.test.ts index a9b38b1d27..04049d77cf 100644 --- a/packages/api/src/controllers/asset.test.ts +++ b/packages/api/src/controllers/asset.test.ts @@ -1,11 +1,20 @@ import serverPromise, { TestServer } from "../test-server"; -import { TestClient, clearDatabase, setupUsers } from "../test-helpers"; +import { + TestClient, + clearDatabase, + setupUsers, + createMockFile, +} from "../test-helpers"; import { v4 as uuid } from "uuid"; import { Asset, User } from "../schema/types"; import { db } from "../store"; import { WithID } from "../store/types"; import Table from "../store/table"; import schema from "../schema/schema.json"; +import fs from "fs/promises"; +import * as tus from "tus-js-client"; +import os from "os"; +import { sleep } from "../util"; // repeat the type here so we don't need to export it from store/asset-table.ts type DBAsset = @@ -316,5 +325,112 @@ describe("controllers/asset", () => { }, }); }); + + describe("chunked upload", () => { + const expectTaskStatus = async ( + taskId: string, + expectedStatus: string + ) => { + const res = await client.get(`/task/${taskId}`); + expect(res.status).toBe(200); + const task = await res.json(); + expect(task.status.phase).toBe(expectedStatus); + }; + + const uploadFile = async ( + filename: string, + filePath: string, + tusEndpoint: string, + shouldAbort: boolean, + resumeFrom?: number + ) => { + const file = await fs.readFile(filePath); + const { size } = await fs.stat(filePath); + let uploadPercentage = await new Promise( + async (resolve, reject) => { + const upload = new tus.Upload(file, { + endpoint: tusEndpoint, + urlStorage: new (tus as any).FileUrlStorage( + `${os.tmpdir()}/metadata` + ), + chunkSize: 1024 * 1024 * 1, + metadata: { + filename, + filetype: "video/mp4", + }, + uploadSize: size, + onError(error) { + reject(error); + }, + onProgress(bytesUploaded, bytesTotal) { + const percentage = parseFloat( + ((bytesUploaded / bytesTotal) * 100).toFixed(2) + ); + if (resumeFrom) { + expect(percentage).toBeGreaterThanOrEqual(resumeFrom); + } + if (shouldAbort && percentage > 1) { + upload.abort().then(() => { + resolve(percentage); + }); + } + }, + onSuccess() { + resolve(100); + }, + }); + if (resumeFrom) { + const previousUploads = await upload.findPreviousUploads(); + expect(previousUploads).toHaveLength(1); + upload.resumeFromPreviousUpload(previousUploads[0]); + } + upload.start(); + } + ); + if (shouldAbort) { + expect(uploadPercentage).toBeGreaterThan(0); + expect(uploadPercentage).toBeLessThan(100); + } else { + expect(uploadPercentage).toBe(100); + } + return uploadPercentage; + }; + + it("should start upload, stop it, resume it on tus test server", async () => { + const filename = "test.mp4"; + const path = os.tmpdir(); + const filePath = `${path}/${filename}`; + let res = await client.post("/asset/request-upload", { + name: "tus-test", + }); + expect(res.status).toBe(200); + let { + tusEndpoint, + task: { id: taskId }, + } = await res.json(); + expect( + tusEndpoint?.startsWith(`http://test/api/asset/upload/tus?token=`) + ).toBe(true); + tusEndpoint = tusEndpoint.replace("http://test", client.server.host); + + await createMockFile(filePath, 1024 * 1024 * 10); + await expectTaskStatus(taskId, "pending"); + let percentage = await uploadFile( + filename, + filePath, + tusEndpoint, + true + ); + await expectTaskStatus(taskId, "pending"); + await uploadFile(filename, filePath, tusEndpoint, false, percentage); + + await sleep(100); + + await expectTaskStatus(taskId, "waiting"); + + await fs.unlink(filePath); + await fs.unlink(`${path}/metadata`); + }); + }); }); }); diff --git a/packages/api/src/controllers/asset.ts b/packages/api/src/controllers/asset.ts index 720fe3d929..4401c075ca 100644 --- a/packages/api/src/controllers/asset.ts +++ b/packages/api/src/controllers/asset.ts @@ -4,6 +4,7 @@ import { Request, RequestHandler, Router } from "express"; import jwt, { JwtPayload } from "jsonwebtoken"; import { v4 as uuid } from "uuid"; import mung from "express-mung"; +import tus from "tus-node-server"; import { makeNextHREF, parseFilters, @@ -21,6 +22,8 @@ import { NotFoundError, BadRequestError, InternalServerError, + UnauthorizedError, + NotImplementedError, } from "../store/errors"; import httpProxy from "http-proxy"; import { generateUniquePlaybackId } from "./generate-keys"; @@ -34,6 +37,9 @@ import { import { WithID } from "../store/types"; import { mergeAssetStatus } from "../store/asset-table"; import Queue from "../store/queue"; +import taskScheduler from "../task/scheduler"; +import { S3ClientConfig } from "@aws-sdk/client-s3"; +import os from "os"; const app = Router(); @@ -65,6 +71,12 @@ async function validateAssetPayload( ): Promise> { validateAssetMeta(payload.meta); if (payload.objectStoreId) { + if (payload.objectStoreId !== defaultObjectStoreId) { + // TODO: Allow assets Object Store to be changed at some point. + throw new UnprocessableEntityError( + `Object store is not customizable right now` + ); + } const os = await db.objectStore.get(payload.objectStoreId); if (os.userId !== userId) { throw new ForbiddenError( @@ -100,6 +112,10 @@ export function getPlaybackUrl(ingest: string, asset: WithID): string { ); } +function getDownloadUrl(ingest: string, asset: WithID): string { + return pathJoin(ingest, "asset", asset.playbackId, "video"); +} + function withPlaybackUrls(ingest: string, asset: WithID): WithID { if (asset.status.phase !== "ready") { return asset; @@ -107,7 +123,7 @@ function withPlaybackUrls(ingest: string, asset: WithID): WithID { return { ...asset, playbackUrl: getPlaybackUrl(ingest, asset), - downloadUrl: pathJoin(ingest, "asset", asset.playbackId, "video"), + downloadUrl: getDownloadUrl(ingest, asset), }; } @@ -169,15 +185,15 @@ async function genUploadUrl( jwtSecret: string, aud: string ) { - const uploadedObjectKey = `directUpload/${playbackId}/source`; + const uploadedObjectKey = `directUpload/${playbackId}`; const presignedUrl = await getS3PresignedUrl( objectStoreId, uploadedObjectKey ); - const signedUploadUrl = jwt.sign({ presignedUrl, aud }, jwtSecret, { + const uploadToken = jwt.sign({ playbackId, presignedUrl, aud }, jwtSecret, { algorithm: "HS256", }); - return { uploadedObjectKey, signedUploadUrl }; + return { uploadedObjectKey, uploadToken }; } function parseUploadUrl( @@ -185,25 +201,18 @@ function parseUploadUrl( jwtSecret: string, audience: string ) { + let urlJwt: JwtPayload; let uploadUrl: string; try { - const urlJwt = jwt.verify(signedUploadUrl, jwtSecret, { + urlJwt = jwt.verify(signedUploadUrl, jwtSecret, { audience, }) as JwtPayload; uploadUrl = urlJwt.presignedUrl; } catch (err) { throw new ForbiddenError(`Invalid signed upload URL: ${err}`); } - - // get playbackId from s3 url - const matches = uploadUrl.match(/\/directUpload\/([^/]+)\/source/); - if (!matches || matches.length < 2) { - throw new UnprocessableEntityError( - `the provided url for the upload is not valid or not supported: ${uploadUrl}` - ); - } - const playbackId = matches[1]; - return { uploadUrl, playbackId }; + const { playbackId } = urlJwt; + return { playbackId, uploadUrl }; } app.use( @@ -511,7 +520,7 @@ app.post( vodObjectStoreId, { name: `asset-upload-${id}`, ...req.body } ); - const { uploadedObjectKey, signedUploadUrl } = await genUploadUrl( + const { uploadedObjectKey, uploadToken } = await genUploadUrl( playbackId, asset.objectStoreId, jwtSecret, @@ -524,7 +533,8 @@ app.post( return res.json({ errors: ["Ingest not configured"] }); } const baseUrl = ingests[0].origin; - const url = `${baseUrl}/api/asset/upload/direct/${signedUploadUrl}`; + const url = `${baseUrl}/api/asset/upload/direct?token=${uploadToken}`; + const tusEndpoint = `${baseUrl}/api/asset/upload/tus?token=${uploadToken}`; asset = await createAsset(asset, req.queue); const task = await req.taskScheduler.createTask( @@ -536,27 +546,95 @@ app.post( asset ); - res.json({ url, asset, task }); + res.json({ url, tusEndpoint, asset, task }); } ); -app.put("/upload/direct/:urlToken", async (req, res) => { - const { - params: { urlToken }, - config: { jwtSecret, jwtAudience }, - } = req; - const { uploadUrl, playbackId } = parseUploadUrl( - urlToken, - jwtSecret, - jwtAudience - ); +let tusServer: tus.Server; - const assets = await db.asset.find({ playbackId }, { useReplica: false }); - if (!assets?.length || !assets[0]?.length) { - throw new NotFoundError(`asset not found`); +export const setupTus = async (objectStoreId: string): Promise => { + tusServer = await createTusServer(objectStoreId); +}; + +async function createTusServer(objectStoreId: string) { + const os = await db.objectStore.get(objectStoreId); + + const url = new URL(os.url); + const [_, vodRegion, vodBucket] = url.pathname.split("/"); + let protocol = url.protocol; + if (protocol.includes("+")) { + protocol = protocol.split("+")[1]; + } + + const opts: tus.S3StoreOptions | S3ClientConfig = { + path: "/upload/tus", + bucket: vodBucket, + accessKeyId: url.username, + secretAccessKey: url.password, + region: vodRegion, + partSize: 8 * 1024 * 1024, + tmpDirPrefix: "tus-tmp-files", + endpoint: `${protocol}//${url.host}`, + namingFunction, + }; + const tusServer = new tus.Server(); + tusServer.datastore = new tus.S3Store(opts as tus.S3StoreOptions); + tusServer.on(tus.EVENTS.EVENT_UPLOAD_COMPLETE, onTusUploadComplete(false)); + return tusServer; +} + +export const setupTestTus = async (): Promise => { + tusServer = await createTestTusServer(); +}; + +async function createTestTusServer() { + const tusTestServer = new tus.Server(); + tusTestServer.datastore = new tus.FileStore({ + path: "/upload/tus", + directory: os.tmpdir(), + namingFunction: (req: Request) => + req.res.getHeader("livepeer-playback-id").toString(), + }); + tusTestServer.on(tus.EVENTS.EVENT_UPLOAD_COMPLETE, onTusUploadComplete(true)); + return tusTestServer; +} + +const namingFunction = (req: Request) => { + const playbackId = req.res.getHeader("livepeer-playback-id").toString(); + if (!playbackId) { + throw new InternalServerError("Missing playbackId in response headers"); } - let asset = assets[0][0]; - if (asset.status.phase !== "waiting") { + return `directUpload/${playbackId}`; +}; + +type TusFileMetadata = { + id: string; + upload_length: `${number}`; + upload_metadata: string; +}; + +const onTusUploadComplete = + (isTest: boolean) => + async ({ file }: { file: TusFileMetadata }) => { + try { + const playbackId = isTest ? file.id : file.id.split("/")[1]; // `directUpload/${playbackId}` + const { task } = await getPendingAssetAndTask(playbackId); + await taskScheduler.enqueueTask(task); + } catch (err) { + console.error( + `error processing finished upload fileId=${file.id} err=`, + err + ); + } + }; + +const getPendingAssetAndTask = async (playbackId: string) => { + const asset = await db.asset.getByPlaybackId(playbackId, { + useReplica: false, + }); + if (!asset) { + throw new NotFoundError(`asset not found`); + } else if (asset.status.phase !== "waiting") { throw new UnprocessableEntityError(`asset has already been uploaded`); } @@ -571,7 +649,48 @@ app.put("/upload/direct/:urlToken", async (req, res) => { if (task.status?.phase !== "pending") { throw new UnprocessableEntityError(`asset has already been uploaded`); } + return { asset, task }; +}; + +app.use("/upload/tus/*", async (req, res, next) => { + if (!tusServer) { + throw new NotImplementedError("Tus server not configured"); + } + return next(); +}); + +app.post("/upload/tus", async (req, res) => { + const uploadToken = req.query.token?.toString(); + if (!uploadToken) { + throw new UnauthorizedError( + "Missing uploadToken metadata from /request-upload API" + ); + } + + const { jwtSecret, jwtAudience } = req.config; + const { playbackId } = parseUploadUrl(uploadToken, jwtSecret, jwtAudience); + await getPendingAssetAndTask(playbackId); + // TODO: Consider updating asset name and meta from metadata? + res.setHeader("livepeer-playback-id", playbackId); + return tusServer.handle(req, res); +}); + +app.all("/upload/tus/*", (req, res) => { + return tusServer.handle(req, res); +}); + +app.put("/upload/direct", async (req, res) => { + const { + query: { token }, + config: { jwtSecret, jwtAudience }, + } = req; + const { uploadUrl, playbackId } = parseUploadUrl( + token?.toString(), + jwtSecret, + jwtAudience + ); + const { task } = await getPendingAssetAndTask(playbackId); var proxy = httpProxy.createProxyServer({}); proxy.on("end", async function (proxyReq, _, res) { if (res.statusCode == 200) { diff --git a/packages/api/src/store/errors.ts b/packages/api/src/store/errors.ts index 1be186494a..54b1ee07ef 100644 --- a/packages/api/src/store/errors.ts +++ b/packages/api/src/store/errors.ts @@ -58,3 +58,11 @@ export class InternalServerError extends APIError { this.status = 500; } } + +export class NotImplementedError extends APIError { + constructor(message) { + super(message); + this.type = "NotImplementedError"; + this.status = 501; + } +} diff --git a/packages/api/src/task/scheduler.ts b/packages/api/src/task/scheduler.ts index b4d3e9726c..9e24676479 100644 --- a/packages/api/src/task/scheduler.ts +++ b/packages/api/src/task/scheduler.ts @@ -13,14 +13,20 @@ const taskInfo = (task: Task): messages.TaskInfo => ({ snapshot: task, }); -export default class TaskScheduler { +export class TaskScheduler { queue: Queue; running: boolean; - constructor({ queue }) { + + constructor() { + // initialized through start to allow for singleton instance + } + + async start({ queue }) { + if (this.running) { + throw new Error("task scheduler already running"); + } this.running = true; this.queue = queue; - } - async start() { await this.queue.consume("task", this.handleTaskQueue.bind(this)); } @@ -300,3 +306,6 @@ export default class TaskScheduler { }); } } + +const taskScheduler = new TaskScheduler(); +export default taskScheduler; diff --git a/packages/api/src/test-helpers.ts b/packages/api/src/test-helpers.ts index f3c7d74b2b..c348eaf983 100644 --- a/packages/api/src/test-helpers.ts +++ b/packages/api/src/test-helpers.ts @@ -5,6 +5,7 @@ import { v4 as uuid } from "uuid"; import schema from "./schema/schema.json"; import { User } from "./schema/types"; import { TestServer } from "./test-server"; +import fs from "fs"; const vhostUrl = (vhost: string) => `http://guest:guest@localhost:15672/api/vhosts/${vhost}`; @@ -238,3 +239,12 @@ export async function setupUsers( nonAdminApiKey, }; } + +export async function createMockFile(fileName: string, size: number) { + return await new Promise((resolve, reject) => { + let fh = fs.openSync(fileName, "w"); + fs.writeSync(fh, "ok", Math.max(0, size - 2)); + fs.closeSync(fh); + resolve(true); + }); +} diff --git a/packages/api/src/test-server.ts b/packages/api/src/test-server.ts index f43a40ebf1..f6d72dd9b4 100644 --- a/packages/api/src/test-server.ts +++ b/packages/api/src/test-server.ts @@ -41,7 +41,7 @@ params.postgresUrl = `postgresql://postgres@127.0.0.1/${testId}`; params.recordObjectStoreId = "mock_store"; params.vodObjectStoreId = "mock_vod_store"; params.ingest = - '[{"ingest": "rtmp://test/live","playback": "https://test/hls","base": "https://test"}]'; + '[{"ingest": "rtmp://test/live","playback": "https://test/hls","base": "https://test","origin":"http://test"}]'; params.amqpUrl = `amqp://localhost:5672/${testId}`; if (!params.insecureTestToken) { params.insecureTestToken = uuid(); diff --git a/packages/api/src/types/common.d.ts b/packages/api/src/types/common.d.ts index 8fc1025195..b07e0bb074 100644 --- a/packages/api/src/types/common.d.ts +++ b/packages/api/src/types/common.d.ts @@ -25,7 +25,6 @@ declare global { taskScheduler?: TaskScheduler; stripe?: Stripe; frontendDomain: string; - user?: User; isUIAdmin?: boolean; token?: WithID; diff --git a/packages/api/src/webhooks/cannon.ts b/packages/api/src/webhooks/cannon.ts index 0268fe8936..91e122c3ff 100644 --- a/packages/api/src/webhooks/cannon.ts +++ b/packages/api/src/webhooks/cannon.ts @@ -11,7 +11,7 @@ import { DBWebhook } from "../store/webhook-table"; import { fetchWithTimeout, RequestInitWithTimeout } from "../util"; import logger from "../logger"; import { sign, sendgridEmail } from "../controllers/helpers"; -import TaskScheduler from "../task/scheduler"; +import taskScheduler from "../task/scheduler"; import { generateUniquePlaybackId } from "../controllers/generate-keys"; import { createAsset } from "../controllers/asset"; import { DBStream } from "../store/stream-table"; @@ -31,7 +31,6 @@ export default class WebhookCannon { sendgridTemplateId: string; sendgridApiKey: string; supportAddr: [string, string]; - taskScheduler: TaskScheduler; vodObjectStoreId: string; resolver: any; queue: Queue; @@ -41,7 +40,6 @@ export default class WebhookCannon { sendgridTemplateId, sendgridApiKey, supportAddr, - taskScheduler, vodObjectStoreId, verifyUrls, queue, @@ -53,7 +51,6 @@ export default class WebhookCannon { this.sendgridTemplateId = sendgridTemplateId; this.sendgridApiKey = sendgridApiKey; this.supportAddr = supportAddr; - this.taskScheduler = taskScheduler; this.vodObjectStoreId = vodObjectStoreId; this.resolver = new dns.Resolver(); this.queue = queue; @@ -526,7 +523,7 @@ export default class WebhookCannon { }, this.queue ); - await this.taskScheduler.scheduleTask( + await taskScheduler.scheduleTask( "import", { import: { diff --git a/yarn.lock b/yarn.lock index 788ab0f4a7..2c92536a88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2975,6 +2975,53 @@ resolved "https://registry.yarnpkg.com/@exodus/schemasafe/-/schemasafe-1.0.0-rc.6.tgz#7985f681564cff4ffaebb5896eb4be20af3aae7a" integrity sha512-dDnQizD94EdBwEj/fh3zPRa/HWCS9O5au2PuHhZBbuM3xWHxuaKzPBOEWze7Nn0xW68MIpZ7Xdyn1CoCpjKCuQ== +"@google-cloud/paginator@^3.0.7": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@google-cloud/paginator/-/paginator-3.0.7.tgz#fb6f8e24ec841f99defaebf62c75c2e744dd419b" + integrity sha512-jJNutk0arIQhmpUUQJPJErsojqo834KcyB6X7a1mxuic8i1tKXxde8E69IZxNZawRIlZdIK2QY4WALvlK5MzYQ== + dependencies: + arrify "^2.0.0" + extend "^3.0.2" + +"@google-cloud/projectify@^2.0.0": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@google-cloud/projectify/-/projectify-2.1.1.tgz#ae6af4fee02d78d044ae434699a630f8df0084ef" + integrity sha512-+rssMZHnlh0twl122gXY4/aCrk0G1acBqkHFfYddtsqpYXGxA29nj9V5V9SfC+GyOG00l650f6lG9KL+EpFEWQ== + +"@google-cloud/promisify@^2.0.0": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@google-cloud/promisify/-/promisify-2.0.4.tgz#9d8705ecb2baa41b6b2673f3a8e9b7b7e1abc52a" + integrity sha512-j8yRSSqswWi1QqUGKVEKOG03Q7qOoZP6/h2zN2YO+F5h2+DHU0bSrHCK9Y7lo2DI9fBd8qGAw795sf+3Jva4yA== + +"@google-cloud/storage@^5.18.1": + version "5.20.5" + resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-5.20.5.tgz#1de71fc88d37934a886bc815722c134b162d335d" + integrity sha512-lOs/dCyveVF8TkVFnFSF7IGd0CJrTm91qiK6JLu+Z8qiT+7Ag0RyVhxZIWkhiACqwABo7kSHDm8FdH8p2wxSSw== + dependencies: + "@google-cloud/paginator" "^3.0.7" + "@google-cloud/projectify" "^2.0.0" + "@google-cloud/promisify" "^2.0.0" + abort-controller "^3.0.0" + arrify "^2.0.0" + async-retry "^1.3.3" + compressible "^2.0.12" + configstore "^5.0.0" + duplexify "^4.0.0" + ent "^2.2.0" + extend "^3.0.2" + gaxios "^4.0.0" + google-auth-library "^7.14.1" + hash-stream-validation "^0.2.2" + mime "^3.0.0" + mime-types "^2.0.8" + p-limit "^3.0.1" + pumpify "^2.0.0" + retry-request "^4.2.2" + stream-events "^1.0.4" + teeny-request "^7.1.3" + uuid "^8.0.0" + xdg-basedir "^4.0.0" + "@hookform/resolvers@2.0.0-beta.3": version "2.0.0-beta.3" resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-2.0.0-beta.3.tgz#6a13cecb8d261fee9fc53564e3e4ef6212148f16" @@ -6652,6 +6699,11 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@tootallnate/once@2": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" + integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== + "@tsconfig/node10@^1.0.7": version "1.0.8" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" @@ -8134,6 +8186,13 @@ async-limiter@~1.0.0: resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== +async-retry@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.3.tgz#0e7f36c04d8478e7a58bdbed80cedf977785f280" + integrity sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw== + dependencies: + retry "0.13.1" + async@^2.1.2, async@^2.4.1, async@^2.6.2: version "2.6.4" resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221" @@ -8207,6 +8266,21 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== +aws-sdk@^2.1064.0: + version "2.1154.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1154.0.tgz#94c9406aeb69b7320a16714eb4958a9569cbb7f4" + integrity sha512-SIxLcWGsnW9Sl2P+a+uoqebBsfjeAZZOQokzgDj3VoESnFzsjI+2REi9CdvvSvwlfFUP7sFr6A0khrYNDJLebQ== + dependencies: + buffer "4.9.2" + events "1.1.1" + ieee754 "1.1.13" + jmespath "0.16.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "8.0.0" + xml2js "0.4.19" + aws-serverless-express@^3.3.6: version "3.4.0" resolved "https://registry.yarnpkg.com/aws-serverless-express/-/aws-serverless-express-3.4.0.tgz#74153b8cc80dbd2c6a32a51e6d353a325c2710d7" @@ -9029,7 +9103,7 @@ buffer-fill@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" integrity sha1-+PeLdniYiO858gXNY39o5wISKyw= -buffer-from@^1.0.0: +buffer-from@^1.0.0, buffer-from@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== @@ -9049,7 +9123,7 @@ buffer-xor@^1.0.3: resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= -buffer@^4.3.0: +buffer@4.9.2, buffer@^4.3.0: version "4.9.2" resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== @@ -9966,6 +10040,14 @@ columnify@^1.5.4: strip-ansi "^6.0.1" wcwidth "^1.0.0" +combine-errors@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/combine-errors/-/combine-errors-3.0.3.tgz#f4df6740083e5703a3181110c2b10551f003da86" + integrity sha512-C8ikRNRMygCwaTx+Ek3Yr+OuZzgZjduCOfSQBjbM8V3MfgcjSTeto/GXP6PAwKvJz/v15b7GHZvx5rOlczFw/Q== + dependencies: + custom-error-instance "2.1.1" + lodash.uniqby "4.5.0" + combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -10079,6 +10161,13 @@ compress-commons@^4.1.0: normalize-path "^3.0.0" readable-stream "^3.6.0" +compressible@^2.0.12: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + compute-scroll-into-view@^1.0.17: version "1.0.17" resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz#6a88f18acd9d42e9cf4baa6bec7e0522607ab7ab" @@ -10136,7 +10225,7 @@ configstore@^3.0.0: write-file-atomic "^2.0.0" xdg-basedir "^3.0.0" -configstore@^5.0.1: +configstore@^5.0.0, configstore@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== @@ -10846,6 +10935,11 @@ currently-unhandled@^0.4.1: dependencies: array-find-index "^1.0.1" +custom-error-instance@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/custom-error-instance/-/custom-error-instance-2.1.1.tgz#3cf6391487a6629a6247eb0ca0ce00081b7e361a" + integrity sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg== + cwd@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/cwd/-/cwd-0.9.1.tgz#41e10a7e1ab833dc59c2eca83814c7de77b5a4fd" @@ -11066,7 +11160,7 @@ debug@3.1.0, debug@=3.1.0: dependencies: ms "2.0.0" -debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1: +debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, debug@^4.3.3: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -11754,6 +11848,11 @@ enquirer@^2.3.6: dependencies: ansi-colors "^4.1.1" +ent@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" + integrity sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA== + entities@2.2.0, entities@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" @@ -12441,6 +12540,11 @@ eventemitter3@^4.0.0, eventemitter3@^4.0.1: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== +events@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + integrity sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw== + events@^3.0.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" @@ -13588,6 +13692,17 @@ gaxios@^2.0.1, gaxios@^2.1.0: is-stream "^2.0.0" node-fetch "^2.3.0" +gaxios@^4.0.0: + version "4.3.3" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-4.3.3.tgz#d44bdefe52d34b6435cc41214fdb160b64abfc22" + integrity sha512-gSaYYIO1Y3wUtdfHmjDUZ8LWaxJQpiavzbF5Kq53akSzvmVg0RfyOcFDbO1KJ/KCGRFz2qG+lS81F0nkr7cRJA== + dependencies: + abort-controller "^3.0.0" + extend "^3.0.2" + https-proxy-agent "^5.0.0" + is-stream "^2.0.0" + node-fetch "^2.6.7" + gcp-metadata@^3.4.0: version "3.5.0" resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-3.5.0.tgz#6d28343f65a6bbf8449886a0c0e4a71c77577055" @@ -13596,6 +13711,14 @@ gcp-metadata@^3.4.0: gaxios "^2.1.0" json-bigint "^0.3.0" +gcp-metadata@^4.2.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-4.3.1.tgz#fb205fe6a90fef2fd9c85e6ba06e5559ee1eefa9" + integrity sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A== + dependencies: + gaxios "^4.0.0" + json-bigint "^1.0.0" + generic-names@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/generic-names/-/generic-names-1.0.3.tgz#2d786a121aee508876796939e8e3bff836c20917" @@ -14100,6 +14223,21 @@ google-auth-library@^5.2.0, google-auth-library@^5.2.2, google-auth-library@^5.6 jws "^4.0.0" lru-cache "^5.0.0" +google-auth-library@^7.14.1: + version "7.14.1" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-7.14.1.tgz#e3483034162f24cc71b95c8a55a210008826213c" + integrity sha512-5Rk7iLNDFhFeBYc3s8l1CqzbEBcdhwR193RlD4vSNFajIcINKI8W8P0JLmBpwymHqqWbX34pJDQu39cSy/6RsA== + dependencies: + arrify "^2.0.0" + base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" + fast-text-encoding "^1.0.0" + gaxios "^4.0.0" + gcp-metadata "^4.2.0" + gtoken "^5.0.4" + jws "^4.0.0" + lru-cache "^6.0.0" + google-p12-pem@^2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-2.0.5.tgz#b1c44164d567ae894f7a19b4ff362a06be5b793b" @@ -14107,6 +14245,13 @@ google-p12-pem@^2.0.0: dependencies: node-forge "^0.10.0" +google-p12-pem@^3.1.3: + version "3.1.4" + resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-3.1.4.tgz#123f7b40da204de4ed1fbf2fd5be12c047fc8b3b" + integrity sha512-HHuHmkLgwjdmVRngf5+gSmpkyaRI6QmOg77J8tkNBHhNEI62sGHyw4/+UkgyZEI7h84NbWprXDJ+sa3xOYFvTg== + dependencies: + node-forge "^1.3.1" + googleapis-common@^3.1.0: version "3.2.2" resolved "https://registry.yarnpkg.com/googleapis-common/-/googleapis-common-3.2.2.tgz#f8631f94b3a5c58d8ce955c3290bb65fdb6d7ba4" @@ -14184,7 +14329,7 @@ got@^9.6.0: to-readable-stream "^1.0.0" url-parse-lax "^3.0.0" -graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.9: +graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4, graceful-fs@^4.2.9: version "4.2.10" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== @@ -14263,6 +14408,15 @@ gtoken@^4.1.0: jws "^4.0.0" mime "^2.2.0" +gtoken@^5.0.4: + version "5.3.2" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-5.3.2.tgz#deb7dc876abe002178e0515e383382ea9446d58f" + integrity sha512-gkvEKREW7dXWF8NV8pVrKfW7WqReAmjjkMBh6lNCCGOM4ucS0r0YyXXl0r/9Yj8wcW/32ISkfc8h5mPTDbtifQ== + dependencies: + gaxios "^4.0.0" + google-p12-pem "^3.1.3" + jws "^4.0.0" + gulp-header@^1.7.1: version "1.8.12" resolved "https://registry.yarnpkg.com/gulp-header/-/gulp-header-1.8.12.tgz#ad306be0066599127281c4f8786660e705080a84" @@ -14434,6 +14588,11 @@ hash-base@^3.0.0: readable-stream "^3.6.0" safe-buffer "^5.2.0" +hash-stream-validation@^0.2.2: + version "0.2.4" + resolved "https://registry.yarnpkg.com/hash-stream-validation/-/hash-stream-validation-0.2.4.tgz#ee68b41bf822f7f44db1142ec28ba9ee7ccb7512" + integrity sha512-Gjzu0Xn7IagXVkSu9cSFuK1fqzwtLwFhNhVL8IFJijRNMgUttFbBSIAzKuSIrsFMO1+g1RlsoN49zPIbwPDMGQ== + hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" @@ -14812,6 +14971,15 @@ http-proxy-agent@^4.0.1: agent-base "6" debug "4" +http-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43" + integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== + dependencies: + "@tootallnate/once" "2" + agent-base "6" + debug "4" + http-proxy-middleware@^0.20.0: version "0.20.0" resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.20.0.tgz#5b128f7207985c4ea91b53fab8ad897a48c690d6" @@ -14956,6 +15124,11 @@ idna-uts46@^1.0.1: dependencies: punycode "^2.1.0" +ieee754@1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + ieee754@^1.1.13, ieee754@^1.1.4: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -16598,6 +16771,11 @@ jest@^27.0.5: import-local "^3.0.2" jest-cli "^27.5.1" +jmespath@0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.16.0.tgz#b15b0a85dfd4d930d43e69ed605943c802785076" + integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw== + join-component@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/join-component/-/join-component-1.1.0.tgz#b8417b750661a392bee2c2537c68b2a9d4977cd5" @@ -16608,6 +16786,11 @@ js-base64@^2.1.9: resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4" integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ== +js-base64@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.2.tgz#816d11d81a8aff241603d19ce5761e13e41d7745" + integrity sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ== + js-beautify@^1.6.4: version "1.14.3" resolved "https://registry.yarnpkg.com/js-beautify/-/js-beautify-1.14.3.tgz#3dd11c949178de7f3bdf3f6f752778d3bed95150" @@ -16804,6 +16987,13 @@ json-bigint@^0.3.0: dependencies: bignumber.js "^9.0.0" +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== + dependencies: + bignumber.js "^9.0.0" + json-buffer@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" @@ -17530,11 +17720,36 @@ lodash._baseeach@^3.0.0: dependencies: lodash.keys "^3.0.0" +lodash._baseiteratee@~4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash._baseiteratee/-/lodash._baseiteratee-4.7.0.tgz#34a9b5543572727c3db2e78edae3c0e9e66bd102" + integrity sha512-nqB9M+wITz0BX/Q2xg6fQ8mLkyfF7MU7eE+MNBNjTHFKeKaZAPEzEg+E8LWxKWf1DQVflNEn9N49yAuqKh2mWQ== + dependencies: + lodash._stringtopath "~4.8.0" + +lodash._basetostring@~4.12.0: + version "4.12.0" + resolved "https://registry.yarnpkg.com/lodash._basetostring/-/lodash._basetostring-4.12.0.tgz#9327c9dc5158866b7fa4b9d42f4638e5766dd9df" + integrity sha512-SwcRIbyxnN6CFEEK4K1y+zuApvWdpQdBHM/swxP962s8HIxPO3alBH5t3m/dl+f4CMUug6sJb7Pww8d13/9WSw== + +lodash._baseuniq@~4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" + integrity sha512-Ja1YevpHZctlI5beLA7oc5KNDhGcPixFhcqSiORHNsp/1QTv7amAXzw+gu4YOvErqVlMVyIJGgtzeepCnnur0A== + dependencies: + lodash._createset "~4.0.0" + lodash._root "~3.0.0" + lodash._bindcallback@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4= +lodash._createset@~4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26" + integrity sha512-GTkC6YMprrJZCYU3zcqZj+jkXkrXzq3IPBcF/fIPpNEAB4hZEtXU8zp/RwKOvZl43NUmwDbyRk3+ZTbeRdEBXA== + lodash._getnative@^3.0.0: version "3.9.1" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" @@ -17545,6 +17760,18 @@ lodash._reinterpolate@^3.0.0: resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= +lodash._root@~3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692" + integrity sha512-O0pWuFSK6x4EXhM1dhZ8gchNtG7JMqBtrHdoUFUWXD7dJnNSUze1GuyQr5sOs0aCvgGeI3o/OJW8f4ca7FDxmQ== + +lodash._stringtopath@~4.8.0: + version "4.8.0" + resolved "https://registry.yarnpkg.com/lodash._stringtopath/-/lodash._stringtopath-4.8.0.tgz#941bcf0e64266e5fc1d66fed0a6959544c576824" + integrity sha512-SXL66C731p0xPDC5LZg4wI5H+dJo/EO4KTqOMwLYCH3+FmmfAKJEZCm6ohGpI+T1xwsDsJCfL4OnhorllvlTPQ== + dependencies: + lodash._basetostring "~4.12.0" + lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" @@ -17729,6 +17956,14 @@ lodash.uniq@4.5.0, lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= +lodash.uniqby@4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniqby/-/lodash.uniqby-4.5.0.tgz#a3a17bbf62eeb6240f491846e97c1c4e2a5e1e21" + integrity sha512-IRt7cfTtHy6f1aRVA5n7kT8rgN3N1nH6MOWLcHfpWG2SH19E3JksLK38MktLxZDhlAjCP9jpIXkOnRXlu6oByQ== + dependencies: + lodash._baseiteratee "~4.7.0" + lodash._baseuniq "~4.6.0" + lodash.uniqueid@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/lodash.uniqueid/-/lodash.uniqueid-4.0.1.tgz#3268f26a7c88e4f4b1758d679271814e31fa5b26" @@ -18593,12 +18828,12 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.52.0: +mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": version "1.52.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== -mime-types@^2.1.12, mime-types@^2.1.14, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: +mime-types@^2.0.8, mime-types@^2.1.12, mime-types@^2.1.14, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -18615,6 +18850,11 @@ mime@^2.1.0, mime@^2.2.0, mime@^2.4.6: resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== +mime@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" + integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== + mimer@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mimer/-/mimer-1.1.0.tgz#2cb67f7093998e772a0e62c090f77daa1b8a2dbe" @@ -19311,6 +19551,11 @@ node-forge@^0.8.5: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.8.5.tgz#57906f07614dc72762c84cef442f427c0e1b86ee" integrity sha512-vFMQIWt+J/7FLNyKouZ9TazT74PRV3wgv9UT4cRjC8BffxFbKXkgIWR42URCPSnHm/QDz6BOlb2Q0U4+VQT67Q== +node-forge@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + node-gyp-build@^4.2.0: version "4.4.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.4.0.tgz#42e99687ce87ddeaf3a10b99dc06abc11021f3f4" @@ -20169,7 +20414,7 @@ p-limit@^2.0.0, p-limit@^2.2.0: dependencies: p-try "^2.0.0" -p-limit@^3.0.2: +p-limit@^3.0.1, p-limit@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== @@ -21978,6 +22223,15 @@ prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.7, object-assign "^4.1.1" react-is "^16.13.1" +proper-lockfile@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz#c8b9de2af6b2f1601067f98e01ac66baa223141f" + integrity sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA== + dependencies: + graceful-fs "^4.2.4" + retry "^0.12.0" + signal-exit "^3.0.2" + property-expr@^2.0.4: version "2.0.5" resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.5.tgz#278bdb15308ae16af3e3b9640024524f4dc02cb4" @@ -22097,6 +22351,15 @@ pumpify@^1.3.3, pumpify@^1.3.5: inherits "^2.0.3" pump "^2.0.0" +pumpify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-2.0.1.tgz#abfc7b5a621307c728b551decbbefb51f0e4aa1e" + integrity sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw== + dependencies: + duplexify "^4.1.1" + inherits "^2.0.3" + pump "^3.0.0" + punycode@1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" @@ -23884,11 +24147,29 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== +retry-request@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-4.2.2.tgz#b7d82210b6d2651ed249ba3497f07ea602f1a903" + integrity sha512-xA93uxUD/rogV7BV59agW/JHPGXeREMWiZc9jhcwY4YdZ7QOtC7qbomYg0n4wyk2lJhggjvKvhNX8wln/Aldhg== + dependencies: + debug "^4.1.1" + extend "^3.0.2" + +retry@0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + retry@^0.10.0: version "0.10.1" resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4" integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q= +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" @@ -24190,6 +24471,11 @@ sassdash@0.9.0: resolved "https://registry.yarnpkg.com/sassdash/-/sassdash-0.9.0.tgz#e2114e80af0c01639d1c6b88a3eb350740cb1169" integrity sha512-1SIJltpUOCMY06uTEWrtXDtfGr39P2huhikbiAMU0v9S4PfL5StxGiz5Oo0HqyVpWZK3mAItYEntDikUSUyXeQ== +sax@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" + integrity sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA== + sax@>=0.6.0, sax@~1.2.1, sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -25119,6 +25405,13 @@ stream-each@^1.1.0: end-of-stream "^1.1.0" stream-shift "^1.0.0" +stream-events@^1.0.4, stream-events@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/stream-events/-/stream-events-1.0.5.tgz#bbc898ec4df33a4902d892333d47da9bf1c406d5" + integrity sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg== + dependencies: + stubs "^3.0.0" + stream-http@^2.7.2: version "2.8.3" resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" @@ -25405,6 +25698,11 @@ strong-log-transformer@^2.0.0: minimist "^1.2.0" through "^2.3.4" +stubs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b" + integrity sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw== + style-loader@^0.20.1: version "0.20.3" resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.20.3.tgz#ebef06b89dec491bcb1fdb3452e913a6fd1c10c4" @@ -25716,6 +26014,17 @@ tdigest@^0.1.1: dependencies: bintrees "1.0.1" +teeny-request@^7.1.3: + version "7.2.0" + resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-7.2.0.tgz#41347ece068f08d741e7b86df38a4498208b2633" + integrity sha512-SyY0pek1zWsi0LRVAALem+avzMLc33MKW/JLLakdP4s9+D7+jHcy5x6P+h94g2QNZsAqQNfX5lsbd3WSeJXrrw== + dependencies: + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + node-fetch "^2.6.1" + stream-events "^1.0.5" + uuid "^8.0.0" + temp-dir@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" @@ -26226,6 +26535,29 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tus-js-client@^3.0.0-0: + version "3.0.0-0" + resolved "https://registry.yarnpkg.com/tus-js-client/-/tus-js-client-3.0.0-0.tgz#73e39744147f2f25e93fab75951be12510c74d07" + integrity sha512-t/lO6cdtj/gpUpvoJPFta+lARdhEEGIAl9peW6bOBwPzWmvIwOI9rW67kVEeN056ZptiCS3tYbe1/lh6fwjQIw== + dependencies: + buffer-from "^1.1.2" + combine-errors "^3.0.3" + is-stream "^2.0.0" + js-base64 "^3.7.2" + lodash.throttle "^4.1.1" + proper-lockfile "^4.1.2" + url-parse "^1.5.7" + +tus-node-server@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tus-node-server/-/tus-node-server-0.6.0.tgz#27af72b45baa3db98c1d46ac836ad42597cb7479" + integrity sha512-TmqOMCsFzUeHPQd0bu9toEKSbJCT7PGDNXH0MqiL1SX61osvhAMy03Xq/AbZDecQ0UOIt5buXWfG9HEsqYU3Ig== + dependencies: + "@google-cloud/storage" "^5.18.1" + aws-sdk "^2.1064.0" + configstore "^5.0.1" + debug "^4.3.3" + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" @@ -26838,7 +27170,7 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" -url-parse@^1.1.9, url-parse@^1.4.3, url-parse@~1.5.1: +url-parse@^1.1.9, url-parse@^1.4.3, url-parse@^1.5.7, url-parse@~1.5.1: version "1.5.10" resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== @@ -26871,6 +27203,14 @@ url-value-parser@^2.0.0: resolved "https://registry.yarnpkg.com/url-value-parser/-/url-value-parser-2.1.0.tgz#fe1ae776122b2eea4bbf284896bbdcd7fc75e1fa" integrity sha512-gIYPWXujdUdwd/9TGCHTf5Vvgw6lOxjE5Q/k+7WNByYyS0vW5WX0k+xuVlhvPq6gRNhzXVv/ezC+OfeAet5Kcw== +url@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" + integrity sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ== + dependencies: + punycode "1.3.2" + querystring "0.2.0" + url@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" @@ -26976,6 +27316,11 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= +uuid@8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c" + integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw== + uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0, uuid@^3.2.1, uuid@^3.3.2, uuid@^3.3.3: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" @@ -27774,6 +28119,14 @@ xml-name-validator@^3.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== +xml2js@0.4.19: + version "0.4.19" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" + integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== + dependencies: + sax ">=0.6.0" + xmlbuilder "~9.0.1" + xml2js@^0.4.15: version "0.4.23" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" @@ -27792,6 +28145,11 @@ xmlbuilder@~11.0.0: resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== +xmlbuilder@~9.0.1: + version "9.0.7" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" + integrity sha512-7YXTQc3P2l9+0rjaUbLwMKRhtmwg1M1eDf6nag7urC7pIPYLD9W/jmzQ4ptRSUbodw5S0jfoGTflLemQibSpeQ== + xmlchars@^2.1.1, xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"