diff --git a/packages/gateway/README.md b/packages/gateway/README.md index a546031279..92d7cdfe4a 100644 --- a/packages/gateway/README.md +++ b/packages/gateway/README.md @@ -32,6 +32,12 @@ One time set up of your cloudflare worker subdomain for dev: You only need to `npm start` for subsequent runs. PR your env config to the `wrangler.toml` to celebrate 🎉 -## API +## High level architecture -TODO +![High level Architecture](./gateway.nft.storage.jpg) + +The IPFS gateway for nft.storage is not "another gateway", but a caching layer for NFT’s that sits on top of existing IPFS public gateways. + +## Persistence + +Several metrics per gateway are persisted to track the performance of each public gateway over time. Moreover, the list of gateways that have previously fetched successfully a given CID are also persisted. diff --git a/packages/gateway/gateway.nft.storage.jpg b/packages/gateway/gateway.nft.storage.jpg new file mode 100644 index 0000000000..d838c4a41c Binary files /dev/null and b/packages/gateway/gateway.nft.storage.jpg differ diff --git a/packages/gateway/package.json b/packages/gateway/package.json index e0c86bb5bf..5fde78036c 100644 --- a/packages/gateway/package.json +++ b/packages/gateway/package.json @@ -10,12 +10,16 @@ "dev": "miniflare --watch --debug", "deploy": "wrangler publish --env production", "pretest": "npm run build", - "test": "npm-run-all -p -r mock:ipfs.io test:worker", + "test": "npm-run-all -p -r mock:ipfs.io mock:cf-ipfs.com test:worker", "test:worker": "ava --verbose test/*.spec.js", - "mock:ipfs.io": "smoke -p 9081 test/mocks/ipfs.io" + "mock:ipfs.io": "smoke -p 9081 test/mocks/ipfs.io", + "mock:cf-ipfs.com": "smoke -p 9082 test/mocks/cf-ipfs.com" }, "dependencies": { + "itty-router": "^2.4.5", "multiformats": "^9.5.2", + "p-any": "^4.0.0", + "p-settle": "^5.0.0", "toucan-js": "^2.5.0" }, "devDependencies": { @@ -25,6 +29,7 @@ "git-rev-sync": "^3.0.1", "miniflare": "^2.0.0-rc.2", "npm-run-all": "^4.1.5", + "p-retry": "^5.0.0", "sade": "^1.7.4", "smoke": "^3.1.1" }, diff --git a/packages/gateway/src/constants.js b/packages/gateway/src/constants.js new file mode 100644 index 0000000000..8833bced56 --- /dev/null +++ b/packages/gateway/src/constants.js @@ -0,0 +1 @@ +export const METRICS_CACHE_MAX_AGE = 10 * 60 // in seconds (10 minutes) diff --git a/packages/gateway/src/cors.js b/packages/gateway/src/cors.js index e957943eee..fe3831c0b1 100644 --- a/packages/gateway/src/cors.js +++ b/packages/gateway/src/cors.js @@ -1,5 +1,20 @@ /* eslint-env serviceworker */ +/** + * @param {import('itty-router').RouteHandler} handler + * @returns {import('itty-router').RouteHandler} + */ +export function withCorsHeaders(handler) { + /** + * @param {Request} request + * @returns {Promise} + */ + return async (request, ...rest) => { + const response = await handler(request, ...rest) + return addCorsHeaders(request, response) + } +} + /** * @param {Request} request * @param {Response} response diff --git a/packages/gateway/src/durable-objects/cids.js b/packages/gateway/src/durable-objects/cids.js new file mode 100644 index 0000000000..cda49e73ab --- /dev/null +++ b/packages/gateway/src/durable-objects/cids.js @@ -0,0 +1,44 @@ +import { normalizeCid } from '../utils/cid.js' + +/** + * @typedef {Object} CidUpdateRequest + * @property {string} cid + * @property {string[]} urls gateway URLs + */ + +/** + * Durable Object for tracking CIDs fetching state. + * For each CID requested, a list of the gateways that successfully fetched it are stored. + */ +export class CidsTracker0 { + constructor(state) { + this.state = state + } + + // Handle HTTP requests from clients. + async fetch(request) { + // Apply requested action. + let url = new URL(request.url) + + if (url.pathname === '/update') { + /** @type {CidUpdateRequest} */ + const data = await request.json() + + await this.state.storage.put(data.cid, data.urls) + return new Response() + } else if (url.pathname.includes('/status')) { + const cid = url.pathname.split('/status/')[1] + const nCid = normalizeCid(cid) + + const stored = await this.state.storage.get(nCid) + + if (stored) { + return new Response(JSON.stringify(stored)) + } + + return new Response('Not found', { status: 404 }) + } + + return new Response('Not found', { status: 404 }) + } +} diff --git a/packages/gateway/src/durable-objects/metrics.js b/packages/gateway/src/durable-objects/metrics.js new file mode 100644 index 0000000000..cec5b3fb89 --- /dev/null +++ b/packages/gateway/src/durable-objects/metrics.js @@ -0,0 +1,143 @@ +/** + * @typedef {Object} GatewayMetrics + * @property {number} totalRequests total number of performed requests + * @property {number} totalResponseTime total response time of the requests + * @property {number} totalFailedRequests total number of requests failed + * @property {number} totalWinnerRequests number of performed requests where faster + * @property {Record} responseTimeHistogram + * + * @typedef {Object} ResponseStats + * @property {string} url gateway URL + * @property {boolean} ok request was successful + * @property {number} [responseTime] number of milliseconds to get response + * @property {boolean} [faster] + */ + +// Key to track total time for fast gateway to respond +const TOTAL_FAST_RESPONSE_TIME = 'totalFastResponseTime' + +/** + * Durable Object for keeping Metrics state. + */ +export class Metrics13 { + constructor(state, env) { + this.state = state + /** @type {Array} */ + this.ipfsGateways = JSON.parse(env.IPFS_GATEWAYS) + + // `blockConcurrencyWhile()` ensures no requests are delivered until initialization completes. + this.state.blockConcurrencyWhile(async () => { + // Get state and initialize if not existing + /** @type {Map} */ + this.gatewayMetrics = new Map() + + // Gateway related metrics + const storedMetricsPerGw = await Promise.all( + this.ipfsGateways.map(async (gw) => { + /** @type {GatewayMetrics} */ + const value = + (await this.state.storage.get(gw)) || createMetricsTracker() + + return { + gw, + value: { ...value }, + } + }) + ) + + storedMetricsPerGw.forEach(({ gw, value }) => { + this.gatewayMetrics.set(gw, value) + }) + + // Total response time + this.totalFastResponseTime = + (await this.state.storage.get(TOTAL_FAST_RESPONSE_TIME)) || 0 + }) + } + + // Handle HTTP requests from clients. + async fetch(request) { + // Apply requested action. + let url = new URL(request.url) + switch (url.pathname) { + case '/update': + const data = await request.json() + // Updated Metrics + this._updateMetrics(data) + // Save updated Metrics + await Promise.all([ + ...this.ipfsGateways.map((gw) => + this.state.storage.put(gw, this.gatewayMetrics.get(gw)) + ), + this.state.storage.put( + TOTAL_FAST_RESPONSE_TIME, + this.totalFastResponseTime + ), + ]) + return new Response() + case '/metrics': + const resp = { + totalFastResponseTime: this.totalFastResponseTime, + ipfsGateways: {}, + } + this.ipfsGateways.forEach((url) => { + resp.ipfsGateways[url] = this.gatewayMetrics.get(url) + }) + + return new Response(JSON.stringify(resp)) + default: + return new Response('Not found', { status: 404 }) + } + } + + /** + * @param {ResponseStats[]} responseStats + */ + _updateMetrics(responseStats) { + responseStats.forEach((stats) => { + const gwMetrics = this.gatewayMetrics.get(stats.url) + + if (!stats.ok) { + // Update request count + gwMetrics.totalRequests += 1 + gwMetrics.totalFailedRequests += 1 + return + } + + // Update request count and response time sum + gwMetrics.totalRequests += 1 + gwMetrics.totalResponseTime += stats.responseTime + + // Update faster count if appropriate + if (stats.faster) { + gwMetrics.totalWinnerRequests += 1 + this.totalFastResponseTime += stats.responseTime + } + + // Update histogram + const gwHistogram = { + ...gwMetrics.responseTimeHistogram, + } + + const histogramCandidate = + histogram.find((h) => stats.responseTime <= h) || + histogram[histogram.length - 1] + gwHistogram[histogramCandidate] += 1 + gwMetrics.responseTimeHistogram = gwHistogram + }) + } +} + +export const histogram = [300, 500, 750, 1000, 1500, 2000, 3000, 5000, 10000] + +function createMetricsTracker() { + const h = histogram.map((h) => [h, 0]) + + return { + totalRequests: 0, + totalResponseTime: 0, + totalFailedRequests: 0, + totalWinnerRequests: 0, + responseTimeHistogram: Object.fromEntries(h), + } +} diff --git a/packages/gateway/src/env.js b/packages/gateway/src/env.js new file mode 100644 index 0000000000..40d8646c1e --- /dev/null +++ b/packages/gateway/src/env.js @@ -0,0 +1,61 @@ +import Toucan from 'toucan-js' +import pkg from '../package.json' + +// TODO: Get Durable object typedef +/** + * @typedef {Object} EnvInput + * @property {string} IPFS_GATEWAYS + * @property {string} VERSION + * @property {string} ENV + * @property {string} [SENTRY_DSN] + * @property {number} [REQUEST_TIMEOUT] + * @property {Object} METRICS + * @property {Object} CIDSTRACKER + * + * @typedef {Object} EnvTransformed + * @property {Array} ipfsGateways + * @property {Object} metricsDurable + * @property {Object} cidsTrackerDurable + * @property {number} REQUEST_TIMEOUT + * @property {Toucan} [sentry] + * + * @typedef {EnvInput & EnvTransformed} Env + */ + +/** + * @param {Request} request + * @param {Env} env + */ +export function envAll(request, env) { + env.sentry = getSentry(request, env) + env.ipfsGateways = JSON.parse(env.IPFS_GATEWAYS) + env.metricsDurable = env.METRICS + env.cidsTrackerDurable = env.CIDSTRACKER + env.REQUEST_TIMEOUT = env.REQUEST_TIMEOUT || 20000 +} + +/** + * Get sentry instance if configured + * + * @param {Request} request + * @param {Env} env + */ +function getSentry(request, env) { + if (!env.SENTRY_DSN) { + return + } + + return new Toucan({ + request, + dsn: env.SENTRY_DSN, + allowedHeaders: ['user-agent'], + allowedSearchParams: /(.*)/, + debug: false, + environment: env.ENV || 'dev', + rewriteFrames: { + root: '/', + }, + release: env.VERSION, + pkg, + }) +} diff --git a/packages/gateway/src/error-handler.js b/packages/gateway/src/error-handler.js index 14386ba3b7..1756b97caa 100644 --- a/packages/gateway/src/error-handler.js +++ b/packages/gateway/src/error-handler.js @@ -1,46 +1,15 @@ -import Toucan from 'toucan-js' -import pkg from '../package.json' - /** * @param {Error & {status?: number;code?: string;}} err - * @param {Request} request - * @param {import('./index').Env} env + * @param {import('./env').Env} env */ -export function errorHandler(err, request, env) { +export function errorHandler(err, env) { console.error(err.stack) const status = err.status || 500 - const sentry = getSentry(request, env) - if (sentry && status >= 500) { - sentry.captureException(err) + if (env.sentry && status >= 500) { + env.sentry.captureException(err) } return new Response(err.message || 'Server Error', { status }) } - -/** - * Get sentry instance if configured - * - * @param {Request} request - * @param {import('./index').Env} env - */ -function getSentry(request, env) { - if (!env.SENTRY_DSN) { - return - } - - return new Toucan({ - request, - dsn: env.SENTRY_DSN, - allowedHeaders: ['user-agent', 'x-client'], - allowedSearchParams: /(.*)/, - debug: false, - environment: env.ENV || 'dev', - rewriteFrames: { - root: '/', - }, - release: env.VERSION, - pkg, - }) -} diff --git a/packages/gateway/src/gateway.js b/packages/gateway/src/gateway.js new file mode 100644 index 0000000000..85082ffe58 --- /dev/null +++ b/packages/gateway/src/gateway.js @@ -0,0 +1,145 @@ +/* eslint-env serviceworker, browser */ + +import pAny from 'p-any' +import pSettle from 'p-settle' + +import { getCidFromSubdomainUrl } from './utils/cid.js' + +const METRICS_ID = 'metrics' +const CIDS_TRACKER_ID = 'cids' + +/** + * @typedef {Object} GatewayResponse + * @property {Response} response + * @property {string} url + * @property {number} responseTime + */ + +/** + * Handle gateway request + * + * @param {Request} request + * @param {import('./env').Env} env + * @param {import('./index').Ctx} ctx + */ +export async function gatewayGet(request, env, ctx) { + const reqUrl = new URL(request.url) + const cid = getCidFromSubdomainUrl(reqUrl) + + const gatewayReqs = env.ipfsGateways.map(async (url, index) => { + const ipfsUrl = new URL('ipfs', url) + const controller = new AbortController() + const startTs = Date.now() + const timer = setTimeout(() => controller.abort(), env.REQUEST_TIMEOUT) + + let response + try { + response = await fetch( + `${ipfsUrl.toString()}/${cid}${reqUrl.pathname || ''}`, + { signal: controller.signal } + ) + } finally { + clearTimeout(timer) + } + + /** @type {GatewayResponse} */ + const gwResponse = { + response, + url, + responseTime: Date.now() - startTs, + } + return gwResponse + }) + + try { + const { response, url } = await pAny(gatewayReqs) + + ctx.waitUntil( + (async () => { + const responses = await pSettle(gatewayReqs) + const successFullResponses = responses.filter( + (r) => r.value?.response?.ok + ) + + await Promise.all([ + updateMetrics(request, env, responses, url), + updateCidsTracker(request, env, successFullResponses, cid), + ]) + })() + ) + + // forward gateway response + return response + } catch (err) { + ctx.waitUntil( + (async () => { + // Update metrics as all requests failed + const responses = await pSettle(gatewayReqs) + await updateMetrics(request, env, responses) + })() + ) + + throw err + } +} + +/** + * @param {Request} request + * @param {import('./env').Env} env + * @param {pSettle.PromiseResult[]} responses + * @param {string} [fasterUrl] + */ +async function updateMetrics(request, env, responses, fasterUrl) { + const id = env.metricsDurable.idFromName(METRICS_ID) + const stub = env.metricsDurable.get(id) + + /** @type {import('./durable-objects/metrics').ResponseStats[]} */ + const responseStats = responses.map((r) => ({ + ok: r?.value?.response?.ok, + url: r?.value?.url, + responseTime: r?.value?.responseTime, + faster: fasterUrl === r?.value?.url, + })) + + await stub.fetch(_getUpdateRequestUrl(request, responseStats)) +} + +/** + * @param {Request} request + * @param {import('./env').Env} env + * @param {pSettle.PromiseResult[]} responses + * @param {string} cid + */ +async function updateCidsTracker(request, env, responses, cid) { + const id = env.cidsTrackerDurable.idFromName(CIDS_TRACKER_ID) + const stub = env.cidsTrackerDurable.get(id) + + /** @type {import('./durable-objects/cids').CidUpdateRequest} */ + const updateRequest = { + cid, + urls: responses.filter((r) => r.isFulfilled).map((r) => r?.value?.url), + } + + await stub.fetch(_getUpdateRequestUrl(request, updateRequest)) +} + +/** + * Get a Request to update a durable object + * + * @param {Request} request + * @param {Object} data + */ +function _getUpdateRequestUrl(request, data) { + const reqUrl = new URL( + 'update', + request.url.startsWith('http') ? request.url : `http://${request.url}` + ) + const headers = new Headers() + headers.append('Content-Type', 'application/json') + + return new Request(reqUrl.toString(), { + headers, + method: 'PUT', + body: JSON.stringify(data), + }) +} diff --git a/packages/gateway/src/index.js b/packages/gateway/src/index.js index 7c61bd1e42..9d788f4d94 100644 --- a/packages/gateway/src/index.js +++ b/packages/gateway/src/index.js @@ -1,45 +1,41 @@ -import { addCorsHeaders } from './cors.js' +/* eslint-env serviceworker */ + +import { Router } from 'itty-router' + +import { gatewayGet } from './gateway.js' +import { metricsGet } from './metrics.js' + +// Export Durable Object namespace from the root module. +export { Metrics13 } from './durable-objects/metrics.js' +export { CidsTracker0 } from './durable-objects/cids.js' + +import { addCorsHeaders, withCorsHeaders } from './cors.js' import { errorHandler } from './error-handler.js' -import { getCidFromSubdomainUrl } from './utils/cid.js' +import { envAll } from './env.js' -/** - * @typedef {Object} Env - * @property {string} IPFS_GATEWAY - * @property {string} VERSION - * @property {string} ENV - * @property {string} [SENTRY_DSN] - */ +const router = Router() -/** - * Handle gateway request - * @param {Request} request - * @param {Env} env - */ -async function handleRequest(request, env) { - const publicGatewayUrl = new URL('ipfs', env.IPFS_GATEWAY) - const url = new URL(request.url) - const cid = getCidFromSubdomainUrl(url) - const response = await fetch( - `${publicGatewayUrl}/${cid}${url.pathname || ''}` - ) - - // forward gateway response - return addCorsHeaders(request, response) -} +router + .all('*', envAll) + .get('/metrics', withCorsHeaders(metricsGet)) + .get('*', withCorsHeaders(gatewayGet)) /** * @param {Error} error * @param {Request} request - * @param {Env} env + * @param {import('./env').Env} env */ function serverError(error, request, env) { - return addCorsHeaders(request, errorHandler(error, request, env)) + return addCorsHeaders(request, errorHandler(error, env)) } +// https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent +/** @typedef {{ waitUntil(p: Promise): void }} Ctx */ + export default { - async fetch(request, env) { + async fetch(request, env, ctx) { try { - return await handleRequest(request, env) + return await router.handle(request, env, ctx) } catch (error) { return serverError(error, request, env) } diff --git a/packages/gateway/src/metrics.js b/packages/gateway/src/metrics.js new file mode 100644 index 0000000000..5fdf67abee --- /dev/null +++ b/packages/gateway/src/metrics.js @@ -0,0 +1,99 @@ +/* global Response caches */ + +import { METRICS_CACHE_MAX_AGE } from './constants.js' +import { histogram } from './durable-objects/metrics.js' + +/** + * @typedef MetricsDurable + * @property {number} totalFastResponseTime + * @property {Record} ipfsGateways + */ + +/** + * Retrieve metrics in prometheus exposition format. + * https://prometheus.io/docs/instrumenting/exposition_formats/ + * @param {Request} request + * @param {import('./env').Env} env + * @param {import('./index').Ctx} ctx + * @returns {Promise} + */ +export async function metricsGet(request, env, ctx) { + // TODO: Set cache + // const cache = caches.default + // let res = await cache.match(request) + + // if (res) { + // return res + // } + let res + + const id = env.metricsDurable.idFromName('metrics') + const stub = env.metricsDurable.get(id) + + const stubResponse = await stub.fetch(request) + /** @type {MetricsDurable} */ + const metricsDurable = await stubResponse.json() + + const metrics = [ + `# HELP nftstorage_gateway_total_response_time Average response time.`, + `# TYPE nftstorage_gateway_total_response_time gauge`, + ...env.ipfsGateways.map( + (gw) => + `nftstorage_gateway_total_response_time{gateway="${gw}",env="${ + env.ENV + }"} ${metricsDurable.ipfsGateways[gw].totalResponseTime || 0}` + ), + `# HELP nftstorage_gateway_total_fastest_response_time Total requests performed.`, + `# TYPE nftstorage_gateway_total_fastest_response_time counter`, + `nftstorage_gateway_total_fastest_response_time{env="${env.ENV}"} ${metricsDurable.totalFastResponseTime}`, + `# HELP nftstorage_gateway_total_requests Total requests performed.`, + `# TYPE nftstorage_gateway_total_requests counter`, + ...env.ipfsGateways.map( + (gw) => + `nftstorage_gateway_total_requests{gateway="${gw}",env="${env.ENV}"} ${metricsDurable.ipfsGateways[gw].totalRequests}` + ), + `# HELP nftstorage_gateway_total_successful_requests Total successful requests.`, + `# TYPE nftstorage_gateway_total_successful_requests counter`, + ...env.ipfsGateways.map( + (gw) => + `nftstorage_gateway_total_successful_requests{gateway="${gw}",env="${ + env.ENV + }"} ${ + metricsDurable.ipfsGateways[gw].totalRequests - + metricsDurable.ipfsGateways[gw].totalFailedRequests + }` + ), + `# HELP nftstorage_gateway_total_failed_requests Total failed requests.`, + `# TYPE nftstorage_gateway_total_failed_requests counter`, + ...env.ipfsGateways.map( + (gw) => + `nftstorage_gateway_total_failed_requests{gateway="${gw}",env="${env.ENV}"} ${metricsDurable.ipfsGateways[gw].totalFailedRequests}` + ), + `# HELP nftstorage_gateway_total_faster_requests Total requests with faster response.`, + `# TYPE nftstorage_gateway_total_faster_requests counter`, + ...env.ipfsGateways.map( + (gw) => + `nftstorage_gateway_total_faster_requests{gateway="${gw}",env="${env.ENV}"} ${metricsDurable.ipfsGateways[gw].totalWinnerRequests}` + ), + `# HELP nftstorage_gateway_requests_per_time`, + `# TYPE nftstorage_gateway_requests_per_time histogram`, + ...histogram.map((t) => { + return env.ipfsGateways + .map( + (gw) => + `nftstorage_gateway_requests_per_time{gateway="${gw}",le="${t}",env="${env.ENV}"} ${metricsDurable.ipfsGateways[gw].responseTimeHistogram[t]}` + ) + .join('\n') + }), + ].join('\n') + + res = new Response(metrics, { + headers: { + 'Cache-Control': `public, max-age=${METRICS_CACHE_MAX_AGE}`, + }, + }) + + // ctx.waitUntil(cache.put(request, res.clone())) + + return res +} diff --git a/packages/gateway/test/cids.spec.js b/packages/gateway/test/cids.spec.js new file mode 100644 index 0000000000..99d5a3e0a5 --- /dev/null +++ b/packages/gateway/test/cids.spec.js @@ -0,0 +1,51 @@ +import test from 'ava' + +import { gateways } from './constants.js' +import { getMiniflare, getCIDsTracker } from './utils.js' + +test.beforeEach((t) => { + // Create a new Miniflare environment for each test + t.context = { + mf: getMiniflare(), + } +}) + +test('Sets gateways that resolved cid', async (t) => { + const { mf } = t.context + const cid = 'bafkreidyeivj7adnnac6ljvzj2e3rd5xdw3revw4da7mx2ckrstapoupoq' + + const p = await mf.dispatchFetch(`http://${cid}.ipfs.localhost:8787`) + await p.waitUntil() + + const ns = await mf.getDurableObjectNamespace(getCIDsTracker()) + const id = ns.idFromName('cids') + const stub = ns.get(id) + const doRes = await stub.fetch(`http://localhost:8787/status/${cid}`) + + const gatewaysWithCid = await doRes.json() + + t.truthy(gateways.length === gatewaysWithCid.length) + gatewaysWithCid.forEach((gw) => { + t.truthy(gateways.includes(gw)) + }) +}) + +test('Only sets gateways that resolved cid successfully', async (t) => { + const { mf } = t.context + const cid = 'bafkreidyeivj7adnnac6ljvzj2e3rd5xdw3revw4da7mx2ckrstapopppq' + + // gateways[1] fails + const p = await mf.dispatchFetch(`http://${cid}.ipfs.localhost:8787`) + await p.waitUntil() + + const ns = await mf.getDurableObjectNamespace(getCIDsTracker()) + const id = ns.idFromName('cids') + const stub = ns.get(id) + const doRes = await stub.fetch(`http://localhost:8787/status/${cid}`) + + const gatewaysWithCid = await doRes.json() + + t.falsy(gateways.length === gatewaysWithCid.length) + t.truthy(gatewaysWithCid.includes(gateways[0])) + t.falsy(gatewaysWithCid.includes(gateways[1])) +}) diff --git a/packages/gateway/test/constants.js b/packages/gateway/test/constants.js new file mode 100644 index 0000000000..ba54fbe743 --- /dev/null +++ b/packages/gateway/test/constants.js @@ -0,0 +1 @@ +export const gateways = ['http://localhost:9081', 'http://localhost:9082'] diff --git a/packages/gateway/test/index.spec.js b/packages/gateway/test/index.spec.js index 792566cf51..6bc54f7875 100644 --- a/packages/gateway/test/index.spec.js +++ b/packages/gateway/test/index.spec.js @@ -1,23 +1,11 @@ import test from 'ava' -import { Miniflare } from 'miniflare' + +import { getMiniflare } from './utils.js' test.beforeEach((t) => { // Create a new Miniflare environment for each test - const mf = new Miniflare({ - // Autoload configuration from `.env`, `package.json` and `wrangler.toml` - envPath: true, - packagePath: true, - wranglerConfigPath: true, - // We don't want to rebuild our worker for each test, we're already doing - // it once before we run all tests in package.json, so disable it here. - // This will override the option in wrangler.toml. - buildCommand: undefined, - wranglerConfigEnv: 'test', - modules: true, - }) - t.context = { - mf, + mf: getMiniflare(), } }) @@ -38,16 +26,16 @@ test('Gets content', async (t) => { const { mf } = t.context const response = await mf.dispatchFetch( - 'https://bafkreidchi5c4c3kwr5rpkvvwnjz3lh44xi2y2lnbldehwmpplgynigidm.ipfs.localhost:8787' + 'https://bafkreidyeivj7adnnac6ljvzj2e3rd5xdw3revw4da7mx2ckrstapoupoq.ipfs.localhost:8787' ) - t.is(await response.text(), 'Hello gateway.nft.storage!') + t.is(await response.text(), 'Hello nft.storage! 😎') }) test('Gets content with path', async (t) => { const { mf } = t.context const response = await mf.dispatchFetch( - 'https://bafkreidchi5c4c3kwr5rpkvvwnjz3lh44xi2y2lnbldehwmpplgynigidm.ipfs.localhost:8787/path' + 'https://bafkreidyeivj7adnnac6ljvzj2e3rd5xdw3revw4da7mx2ckrstapoupoq.ipfs.localhost:8787/path' ) t.is(await response.text(), 'Hello gateway.nft.storage resource!') }) diff --git a/packages/gateway/test/metrics.spec.js b/packages/gateway/test/metrics.spec.js new file mode 100644 index 0000000000..7ebc6b70e5 --- /dev/null +++ b/packages/gateway/test/metrics.spec.js @@ -0,0 +1,152 @@ +import test from 'ava' + +import { gateways } from './constants.js' +import { getMiniflare } from './utils.js' + +test.beforeEach((t) => { + // Create a new Miniflare environment for each test + t.context = { + mf: getMiniflare(), + } +}) + +test('Gets Metrics content when empty state', async (t) => { + const { mf } = t.context + + const response = await mf.dispatchFetch('http://localhost:8787/metrics') + const metricsResponse = await response.text() + + t.is( + metricsResponse.includes('nftstorage_gateway_total_fastest_response_time'), + true + ) + gateways.forEach((gw) => { + t.is( + metricsResponse.includes(`_total_requests{gateway="${gw}",env="test"} 0`), + true + ) + t.is( + metricsResponse.includes( + `_total_response_time{gateway="${gw}",env="test"}` + ), + true + ) + t.is( + metricsResponse.includes( + `_total_failed_requests{gateway="${gw}",env="test"} 0` + ), + true + ) + t.is( + metricsResponse.includes( + `_total_faster_requests{gateway="${gw}",env="test"} 0` + ), + true + ) + t.is( + metricsResponse.includes(`_requests_per_time{gateway="${gw}",le=`), + true + ) + }) +}) + +test('Gets Metrics content', async (t) => { + const { mf } = t.context + + // Trigger two requests + const p = await Promise.all([ + mf.dispatchFetch( + 'http://bafkreidyeivj7adnnac6ljvzj2e3rd5xdw3revw4da7mx2ckrstapoupoq.ipfs.localhost:8787' + ), + mf.dispatchFetch( + 'http://bafkreidyeivj7adnnac6ljvzj2e3rd5xdw3revw4da7mx2ckrstapoupoq.ipfs.localhost:8787' + ), + ]) + + // Wait for waitUntil + await Promise.all(p.map((p) => p.waitUntil())) + + const response = await mf.dispatchFetch('http://localhost:8787/metrics') + const metricsResponse = await response.text() + + gateways.forEach((gw) => { + t.is( + metricsResponse.includes(`_total_requests{gateway="${gw}",env="test"} 2`), + true + ) + }) +}) + +test('Gets Metrics from faster gateway', async (t) => { + const { mf } = t.context + + // Trigger two requests for a CID where gateways[0] is faster + const p = await Promise.all([ + mf.dispatchFetch( + 'http://bafkreidyeivj7adnnac6ljvzj2e3rd5xdw3revw4da7mx2ckrstapouppq.ipfs.localhost:8787' + ), + mf.dispatchFetch( + 'http://bafkreidyeivj7adnnac6ljvzj2e3rd5xdw3revw4da7mx2ckrstapouppq.ipfs.localhost:8787' + ), + ]) + + // Wait for waitUntil + await Promise.all(p.map((p) => p.waitUntil())) + + const response = await mf.dispatchFetch('http://localhost:8787/metrics') + const metricsResponse = await response.text() + + gateways.forEach((gw) => { + t.is( + metricsResponse.includes(`_total_requests{gateway="${gw}",env="test"} 2`), + true + ) + }) + + // gateways[0] is always faster + t.is( + metricsResponse.includes( + `_total_faster_requests{gateway="${gateways[0]}",env="test"} 2` + ), + true + ) + t.is( + metricsResponse.includes( + `_total_faster_requests{gateway="${gateways[1]}",env="test"} 0` + ), + true + ) +}) + +test('Counts failures', async (t) => { + const { mf } = t.context + + // Trigger two requests for a CID where gateways[1] fails + const p = await Promise.all([ + mf.dispatchFetch( + 'http://bafkreidyeivj7adnnac6ljvzj2e3rd5xdw3revw4da7mx2ckrstapopppq.ipfs.localhost:8787' + ), + mf.dispatchFetch( + 'http://bafkreidyeivj7adnnac6ljvzj2e3rd5xdw3revw4da7mx2ckrstapopppq.ipfs.localhost:8787' + ), + ]) + + // Wait for waitUntil + await Promise.all(p.map((p) => p.waitUntil())) + + const response = await mf.dispatchFetch('http://localhost:8787/metrics') + const metricsResponse = await response.text() + + t.is( + metricsResponse.includes( + `_total_requests{gateway="${gateways[1]}",env="test"} 2` + ), + true + ) + t.is( + metricsResponse.includes( + `_total_failed_requests{gateway="${gateways[1]}",env="test"} 2` + ), + true + ) +}) diff --git a/packages/gateway/test/mocks/cf-ipfs.com/get_ipfs#@cid#path.js b/packages/gateway/test/mocks/cf-ipfs.com/get_ipfs#@cid#path.js new file mode 100644 index 0000000000..b72f5bd93c --- /dev/null +++ b/packages/gateway/test/mocks/cf-ipfs.com/get_ipfs#@cid#path.js @@ -0,0 +1,10 @@ +/** + * https://github.com/sinedied/smoke#javascript-mocks + */ +module.exports = () => { + return { + statusCode: 200, + headers: { 'Content-Type': 'text/plain' }, + body: 'Hello gateway.nft.storage resource!', + } +} diff --git a/packages/gateway/test/mocks/cf-ipfs.com/get_ipfs#@cid.js b/packages/gateway/test/mocks/cf-ipfs.com/get_ipfs#@cid.js new file mode 100644 index 0000000000..1d292cfae7 --- /dev/null +++ b/packages/gateway/test/mocks/cf-ipfs.com/get_ipfs#@cid.js @@ -0,0 +1,32 @@ +/** + * https://github.com/sinedied/smoke#javascript-mocks + */ +module.exports = async ({ params }) => { + const cid = params.cid + + if (cid === 'bafkreidyeivj7adnnac6ljvzj2e3rd5xdw3revw4da7mx2ckrstapoupoq') { + return { + statusCode: 200, + headers: { 'Content-Type': 'text/plain' }, + body: 'Hello nft.storage! 😎', + } + } else if ( + cid === 'bafkreidyeivj7adnnac6ljvzj2e3rd5xdw3revw4da7mx2ckrstapouppq' + ) { + // Delays 300ms + await new Promise((resolve) => setTimeout(resolve, 300)) + return { + statusCode: 200, + headers: { 'Content-Type': 'text/plain' }, + body: 'Hello nft.storage! 😎👻', + } + } + + return { + statusCode: 524, + headers: { 'Content-Type': 'text/plain' }, + body: { + error: 'A timeout occurred', + }, + } +} diff --git a/packages/gateway/test/mocks/ipfs.io/get_ipfs#@cid.js b/packages/gateway/test/mocks/ipfs.io/get_ipfs#@cid.js index 244f1fa368..d5807eb30d 100644 --- a/packages/gateway/test/mocks/ipfs.io/get_ipfs#@cid.js +++ b/packages/gateway/test/mocks/ipfs.io/get_ipfs#@cid.js @@ -1,10 +1,26 @@ /** * https://github.com/sinedied/smoke#javascript-mocks */ -module.exports = () => { +module.exports = async ({ params }) => { + const cid = params.cid + + if (cid === 'bafkreidyeivj7adnnac6ljvzj2e3rd5xdw3revw4da7mx2ckrstapoupoq') { + return { + statusCode: 200, + headers: { 'Content-Type': 'text/plain' }, + body: 'Hello nft.storage! 😎', + } + } else if ( + cid === 'bafkreidyeivj7adnnac6ljvzj2e3rd5xdw3revw4da7mx2ckrstapouppq' + ) { + return { + statusCode: 200, + headers: { 'Content-Type': 'text/plain' }, + body: 'Hello nft.storage! 😎👻', + } + } return { - statusCode: 200, + statusCode: 500, headers: { 'Content-Type': 'text/plain' }, - body: 'Hello gateway.nft.storage!', } } diff --git a/packages/gateway/test/utils.js b/packages/gateway/test/utils.js new file mode 100644 index 0000000000..b4db71c6b0 --- /dev/null +++ b/packages/gateway/test/utils.js @@ -0,0 +1,24 @@ +import { Miniflare } from 'miniflare' + +export function getMiniflare() { + return new Miniflare({ + // Autoload configuration from `.env`, `package.json` and `wrangler.toml` + envPath: true, + packagePath: true, + wranglerConfigPath: true, + // We don't want to rebuild our worker for each test, we're already doing + // it once before we run all tests in package.json, so disable it here. + // This will override the option in wrangler.toml. + buildCommand: undefined, + wranglerConfigEnv: 'test', + modules: true, + durableObjects: { + METRICS: 'Metrics13', + CIDSTRACKER: 'CidsTracker0', + }, + }) +} + +export function getCIDsTracker() { + return 'CIDSTRACKER' +} diff --git a/packages/gateway/wrangler.toml b/packages/gateway/wrangler.toml index 7e9a1e23b0..6c80321791 100644 --- a/packages/gateway/wrangler.toml +++ b/packages/gateway/wrangler.toml @@ -16,26 +16,88 @@ format = "modules" dir = "dist" main = "index.mjs" +[durable_objects] +bindings = [{name = "METRICS", class_name = "Metrics13"}, {name = "CIDSTRACKER", class_name = "CidsTracker0"}] + # PROD! [env.production] # name = "gateway-nft-storage-production" account_id = "fffa4b4363a7e5250af8357087263b3a" # Protocol Labs CF account zone_id = "fc6cb51dbc2d0b9a729eae6a302a49c9" # nft.storage zone route = "*gateway.nft.storage/*" -vars = { IPFS_GATEWAY = "https://ipfs.io" } +vars = { IPFS_GATEWAYS = "[\"https://ipfs.io\", \"https://cf-ipfs.com\", \"https://nft-storage.mypinata.cloud/\"]", ENV = "production" } [env.staging] # name = "gateway-nft-storage-staging" account_id = "fffa4b4363a7e5250af8357087263b3a" # Protocol Labs CF account zone_id = "fc6cb51dbc2d0b9a729eae6a302a49c9" # nft.storage zone route = "*gateway-staging.nft.storage/*" -vars = { IPFS_GATEWAY = "https://ipfs.io" } +vars = { IPFS_GATEWAYS = "[\"https://ipfs.io\", \"https://cf-ipfs.com\", \"https://nft-storage.mypinata.cloud/\"]", ENV = "staging" } + +[env.staging.durable_objects] +bindings = [{name = "METRICS", class_name = "Metrics13"}, {name = "CIDSTRACKER", class_name = "CidsTracker0"}] [env.test] workers_dev = true -vars = { IPFS_GATEWAY = "http://localhost:9081" } +vars = { IPFS_GATEWAYS = "[\"http://localhost:9081\", \"http://localhost:9082\"]", ENV = "test" } + +[env.test.durable_objects] +bindings = [{name = "METRICS", class_name = "Metrics13"}, {name = "CIDSTRACKER", class_name = "CidsTracker0"}] [env.vsantos] workers_dev = true account_id = "7ec0b7cf2ec201b2580374e53ba5f37b" -vars = { IPFS_GATEWAY = "https://ipfs.io" } +vars = { IPFS_GATEWAYS = "[\"https://ipfs.io\"]" } + +[env.vsantos.durable_objects] +bindings = [{name = "METRICS", class_name = "Metrics13"}, {name = "CIDSTRACKER", class_name = "CidsTracker0"}] + +[[migrations]] +tag = "v1" # Should be unique for each entry +new_classes = ["Metrics"] +[[migrations]] +tag = "v2" # Should be unique for each entry +new_classes = ["Metrics2"] +[[migrations]] +tag = "v3" # Should be unique for each entry +new_classes = ["Metrics3"] +[[migrations]] +tag = "v4" # Should be unique for each entry +new_classes = ["Metrics4"] +deleted_classes = ["Metrics", "Metrics2", "Metrics3"] +[[migrations]] +tag = "v5" # Should be unique for each entry +new_classes = ["Metrics5"] +deleted_classes = ["Metrics4"] +[[migrations]] +tag = "v6" # Should be unique for each entry +new_classes = ["Metrics6"] +deleted_classes = ["Metrics5"] +[[migrations]] +tag = "v7" # Should be unique for each entry +new_classes = ["Metrics7", "Cids0"] +deleted_classes = ["Metrics6"] +[[migrations]] +tag = "v8" # Should be unique for each entry +new_classes = ["Metrics8", "Cids1"] +deleted_classes = ["Metrics7", "Cids0"] +[[migrations]] +tag = "v9" # Should be unique for each entry +new_classes = ["Metrics9", "CidsTracker0"] +deleted_classes = ["Metrics8", "Cids1"] +[[migrations]] +tag = "v10" # Should be unique for each entry +new_classes = ["Metrics10"] +deleted_classes = ["Metrics9"] +[[migrations]] +tag = "v11" # Should be unique for each entry +new_classes = ["Metrics11"] +deleted_classes = ["Metrics10"] +[[migrations]] +tag = "v12" # Should be unique for each entry +new_classes = ["Metrics12"] +deleted_classes = ["Metrics11"] +[[migrations]] +tag = "v13" # Should be unique for each entry +new_classes = ["Metrics13"] +deleted_classes = ["Metrics12"] \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index a306b4539d..954bd04ec4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3252,7 +3252,7 @@ dependencies: "@types/node" "*" -"@types/retry@^0.12.0": +"@types/retry@^0.12.0", "@types/retry@^0.12.1": version "0.12.1" resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.1.tgz#d8f1c0d0dc23afad6dc16a9e993a0865774b4065" integrity sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g== @@ -3754,6 +3754,14 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" +aggregate-error@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-4.0.0.tgz#83dbdb53a0d500721281d22e19eee9bc352a89cd" + integrity sha512-8DGp7zUt1E9k0NE2q4jlXHk+V3ORErmwolEdRz9iV+LKJ40WhMHh92cxAvhqV2I+zEn/gotIoqoMs0NjF3xofg== + dependencies: + clean-stack "^4.0.0" + indent-string "^5.0.0" + ajv-formats@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" @@ -3884,7 +3892,7 @@ ansi-styles@^2.2.1: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= -ansi-styles@^3.2.1: +ansi-styles@^3.1.0, ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== @@ -4243,6 +4251,13 @@ axe-core@^4.3.5: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.3.5.tgz#78d6911ba317a8262bfee292aeafcc1e04b49cc5" integrity sha512-WKTW1+xAzhMS5dJsxWkliixlO/PqC4VhmO9T4juNYcaTg9jzWiJsou6m5pxWYGfigWbwzJWeFY6z47a+4neRXA== +axios@0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.3.tgz#f85d9b747f9b66d59ca463605cedf1844872b82e" + integrity sha512-JtoZ3Ndke/+Iwt5n+BgSli/3idTvpt5OjKyoCmz4LX5+lPiY5l7C1colYezhlxThjNa/NhngCUWZSZFypIFuaA== + dependencies: + follow-redirects "^1.14.0" + axios@^0.21.1: version "0.21.4" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" @@ -4382,7 +4397,7 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -bl@^4.1.0: +bl@^4.0.3, bl@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== @@ -4544,6 +4559,14 @@ brorand@^1.0.1, brorand@^1.1.0: resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= +brotli-size@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/brotli-size/-/brotli-size-0.1.0.tgz#a2c518096c7c1a75e9e66908a42cd9dc77d2b69f" + integrity sha512-5ny7BNvpe2TSmdafF1T9dnFYp3AIrJ8qJt29K0DQJzORlK38LBim/CmlY26JtreV6SWmXza7Oa+9m61SzvxR0Q== + dependencies: + duplexer "^0.1.1" + iltorb "^2.4.3" + browser-readablestream-to-it@^1.0.1, browser-readablestream-to-it@^1.0.2, browser-readablestream-to-it@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/browser-readablestream-to-it/-/browser-readablestream-to-it-1.0.3.tgz#ac3e406c7ee6cdf0a502dd55db33bab97f7fba76" @@ -4742,6 +4765,22 @@ builtin-status-codes@^3.0.0: resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= +bundlesize@^0.18.1: + version "0.18.1" + resolved "https://registry.yarnpkg.com/bundlesize/-/bundlesize-0.18.1.tgz#17b5c158e7a0d01d70c65c616d87783dfd4b55f3" + integrity sha512-NAsKBH6BeVmDopoa4tod0m5/koM7iLY3saKyGn7wyAravBYmKNUpDJba4zyVhwRm5Dw9WXv8FIO0N//tCkx68Q== + dependencies: + axios "^0.21.1" + brotli-size "0.1.0" + bytes "^3.1.0" + ci-env "^1.4.0" + commander "^2.20.0" + cosmiconfig "^5.2.1" + github-build "^1.2.2" + glob "^7.1.4" + gzip-size "^4.0.0" + prettycli "^1.4.3" + busboy@^0.2.11: version "0.2.14" resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453" @@ -4839,6 +4878,25 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" +caller-callsite@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" + integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ= + dependencies: + callsites "^2.0.0" + +caller-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" + integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ= + dependencies: + caller-callsite "^2.0.0" + +callsites@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" + integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= + callsites@^3.0.0, callsites@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -4928,6 +4986,15 @@ ccount@^1.0.0, ccount@^1.0.3: resolved "https://registry.yarnpkg.com/ccount/-/ccount-1.1.0.tgz#246687debb6014735131be8abab2d93898f8d043" integrity sha512-vlNK021QdI7PNeiUh/lKkC/mNHHfV0m/Ad5JoI0TYtlBnJAslM/JIkm/tGC88bkLIwO6OQ5uV6ztS6kVAtCDlg== +chalk@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.1.0.tgz#ac5becf14fa21b99c6c92ca7a7d7cfd5b17e743e" + integrity sha512-LUHGS/dge4ujbXMJrnihYMcL4AoOweGnw9Tp3kQuqy1Kx5c1qKjqvMJZ6nVJPMWJtKCTN72ZogH3oeSO9g9rXQ== + dependencies: + ansi-styles "^3.1.0" + escape-string-regexp "^1.0.5" + supports-color "^4.0.0" + chalk@2.4.2, chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -5036,6 +5103,11 @@ chokidar@3.5.2, chokidar@^3.4.2, chokidar@^3.4.3, chokidar@^3.5.2: optionalDependencies: fsevents "~2.3.2" +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + chrome-trace-event@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" @@ -5046,6 +5118,11 @@ chunkd@^2.0.1: resolved "https://registry.yarnpkg.com/chunkd/-/chunkd-2.0.1.tgz#49cd1d7b06992dc4f7fccd962fe2a101ee7da920" integrity sha512-7d58XsFmOq0j6el67Ug9mHf9ELUXsQXYJBkyxhH/k+6Ke0qXRnv0kbemx+Twc6fRJ07C49lcbdgm9FL1Ei/6SQ== +ci-env@^1.4.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/ci-env/-/ci-env-1.17.0.tgz#386b8821ce5da1cd0b2b6a06c1f52b1433f8889a" + integrity sha512-NtTjhgSEqv4Aj90TUYHQLxHdnCPXnjdtuGG1X8lTfp/JqeXTdw0FTWl/vUAPuvbWZTF8QVpv6ASe/XacE+7R2A== + ci-info@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" @@ -5096,6 +5173,13 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== +clean-stack@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-4.1.0.tgz#5ce5a2fd19a12aecdce8570daefddb7ac94b6b4e" + integrity sha512-dxXQYI7mfQVcaF12s6sjNFoZ6ZPDQuBBLp3QJ5156k9EvUFClUoZ11fo8HnLQO241DDVntHEug8MOuFO5PSfRg== + dependencies: + escape-string-regexp "5.0.0" + clean-yaml-object@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/clean-yaml-object/-/clean-yaml-object-0.1.0.tgz#63fb110dc2ce1a84dc21f6d9334876d010ae8b68" @@ -5500,6 +5584,16 @@ cors@2.8.5: object-assign "^4" vary "^1" +cosmiconfig@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" + integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== + dependencies: + import-fresh "^2.0.0" + is-directory "^0.3.1" + js-yaml "^3.13.1" + parse-json "^4.0.0" + cosmiconfig@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-6.0.0.tgz#da4fee853c52f6b1e6935f41c1a2fc50bd4a9982" @@ -5954,6 +6048,13 @@ decompress-response@^3.3.0: dependencies: mimic-response "^1.0.0" +decompress-response@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" + integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== + dependencies: + mimic-response "^2.0.0" + decompress-response@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" @@ -6139,6 +6240,11 @@ detab@2.0.4: dependencies: repeat-string "^1.5.4" +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= + detect-node@^2.0.4, detect-node@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" @@ -6464,7 +6570,7 @@ encoding@0.1.13, encoding@^0.1.11, encoding@^0.1.13: dependencies: iconv-lite "^0.6.2" -end-of-stream@^1.1.0: +end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -6990,6 +7096,11 @@ escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +escape-string-regexp@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" + integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== + escape-string-regexp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" @@ -7476,6 +7587,11 @@ exit-on-epipe@~1.0.1: resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692" integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw== +expand-template@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" + integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== + express-http-proxy@^1.6.2: version "1.6.3" resolved "https://registry.yarnpkg.com/express-http-proxy/-/express-http-proxy-1.6.3.tgz#f3ef139ffd49a7962e7af0462bbcca557c913175" @@ -7916,6 +8032,11 @@ fromentries@^1.2.0: resolved "https://registry.yarnpkg.com/fromentries/-/fromentries-1.3.2.tgz#e4bca6808816bf8f93b52750f1127f5a6fd86e3a" integrity sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg== +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + fs-extra@^10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" @@ -8051,6 +8172,18 @@ git-rev-sync@^3.0.1: graceful-fs "4.1.15" shelljs "0.8.4" +github-build@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/github-build/-/github-build-1.2.3.tgz#1f61bf6edb789370f291e7f0e081236ed0fbb830" + integrity sha512-57zUA9ZbaKQHxoUATq3dkr+gUeaOWGGC/3Vw/AJNIUkiUmd7DnYM9TMTmUknbkuvx6+SeSqWpLBunZZzCPLUMg== + dependencies: + axios "0.21.3" + +github-from-package@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" + integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4= + github-slugger@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.4.0.tgz#206eb96cdb22ee56fdc53a28d5a302338463444e" @@ -8363,6 +8496,14 @@ growl@1.10.5: resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== +gzip-size@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-4.1.0.tgz#8ae096257eabe7d69c45be2b67c448124ffb517c" + integrity sha1-iuCWJX6r59acRb4rZ8RIEk/7UXw= + dependencies: + duplexer "^0.1.1" + pify "^3.0.0" + gzip-size@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274" @@ -8416,6 +8557,11 @@ has-dynamic-import@^2.0.0: call-bind "^1.0.2" get-intrinsic "^1.1.1" +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + integrity sha1-6CB68cx7MNRGzHC3NLXovhj4jVE= + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -8883,6 +9029,17 @@ ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== +iltorb@^2.4.3: + version "2.4.5" + resolved "https://registry.yarnpkg.com/iltorb/-/iltorb-2.4.5.tgz#d64434b527099125c6839ed48b666247a172ef87" + integrity sha512-EMCMl3LnnNSZJS5QrxyZmMTaAC4+TJkM5woD+xbpm9RB+mFYCr7C05GFE3TEGCsVQSVHmjX+3sf5AiwsylNInQ== + dependencies: + detect-libc "^1.0.3" + nan "^2.14.0" + npmlog "^4.1.2" + prebuild-install "^5.3.3" + which-pm-runs "^1.0.0" + image-size@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/image-size/-/image-size-1.0.0.tgz#58b31fe4743b1cec0a0ac26f5c914d3c5b2f0750" @@ -8905,6 +9062,14 @@ immutable@^3.x.x: resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3" integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM= +import-fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" + integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY= + dependencies: + caller-path "^2.0.0" + resolve-from "^3.0.0" + import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1, import-fresh@^3.2.2, import-fresh@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -8941,6 +9106,11 @@ indent-string@^4.0.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== +indent-string@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-5.0.0.tgz#4fd2980fccaf8622d14c64d694f4cf33c81951a5" + integrity sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg== + infima@0.2.0-alpha.36: version "0.2.0-alpha.36" resolved "https://registry.yarnpkg.com/infima/-/infima-0.2.0-alpha.36.tgz#205515680e2dc588ec1a07b6ce108e49b29cc810" @@ -9589,6 +9759,11 @@ is-decimal@^1.0.0: resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5" integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw== +is-directory@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= + is-docker@^2.0.0, is-docker@^2.1.1: version "2.2.1" resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" @@ -10206,6 +10381,11 @@ iterall@^1.1.3, iterall@^1.2.1, iterall@^1.2.2: resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea" integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg== +itty-router@^2.4.5: + version "2.4.8" + resolved "https://registry.yarnpkg.com/itty-router/-/itty-router-2.4.8.tgz#6b2ab34268a3516ea0112139dc2cca83cdedf2ce" + integrity sha512-VpAHeAN+/ABLqT/zEnwMomNCDh8eRrs06C8HRR8OxjVwGhXtvzoJbggjUnjAeNGxqjV4w7pp87Iu8jzEpr92fg== + jest-worker@27.0.0-next.5: version "27.0.0-next.5" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.0.0-next.5.tgz#5985ee29b12a4e191f4aae4bb73b97971d86ec28" @@ -11271,6 +11451,11 @@ mimic-response@^1.0.0, mimic-response@^1.0.1: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== +mimic-response@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" + integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== + mimic-response@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" @@ -11356,11 +11541,16 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@^1.2.0, minimist@^1.2.5: +minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + mkdirp@^0.5.1, mkdirp@^0.5.4, mkdirp@^0.5.5: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" @@ -11568,6 +11758,11 @@ mustache@^4.2.0: resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64" integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ== +nan@^2.14.0: + version "2.15.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" + integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== + nano-time@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/nano-time/-/nano-time-1.0.0.tgz#b0554f69ad89e22d0907f7a12b0993a5d96137ef" @@ -11585,6 +11780,11 @@ nanoid@^3.0.2, nanoid@^3.1.20, nanoid@^3.1.23, nanoid@^3.1.30: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362" integrity sha512-zJpuPDwOv8D2zq2WRoMe1HsfZthVewpel9CAvTfc/2mBD1uUT/agc5f7GHGWXlYkFvi1mVxe4IjvP2HNrop7nQ== +napi-build-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" + integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== + napi-macros@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b" @@ -11738,6 +11938,13 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" +node-abi@^2.7.0: + version "2.30.1" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.30.1.tgz#c437d4b1fe0e285aaf290d45b45d4d7afedac4cf" + integrity sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w== + dependencies: + semver "^5.4.1" + node-addon-api@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" @@ -11813,6 +12020,11 @@ node-releases@^2.0.1: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== +noop-logger@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2" + integrity sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI= + normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" @@ -11875,7 +12087,7 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -npmlog@^4.1.2: +npmlog@^4.0.1, npmlog@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== @@ -12152,6 +12364,14 @@ os-browserify@0.3.0: resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= +p-any@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-any/-/p-any-4.0.0.tgz#0e9c8b0fa3e58cc79e6a1c6c715aa9326b6a4447" + integrity sha512-S/B50s+pAVe0wmEZHmBs/9yJXeZ5KhHzOsgKzt0hRdgkoR3DxW9ts46fcsWi/r3VnzsnkKS7q4uimze+zjdryw== + dependencies: + p-cancelable "^3.0.0" + p-some "^6.0.0" + p-cancelable@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" @@ -12162,6 +12382,11 @@ p-cancelable@^2.0.0: resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== +p-cancelable@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-3.0.0.tgz#63826694b54d61ca1c20ebcb6d3ecf5e14cd8050" + integrity sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw== + p-defer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" @@ -12288,6 +12513,14 @@ p-retry@^4.5.0, p-retry@^4.6.1: "@types/retry" "^0.12.0" retry "^0.13.1" +p-retry@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-5.0.0.tgz#012d6e2d4a8774d151d15ac7ecdd98b63d65bbef" + integrity sha512-swGFiU6Y1Q3rBikAGHpaT0FHSbiO9H04fSsJRKVtWyEQMAe2Sb1uXeBcqE/RlZqt2prlq4W2HA/+MZAt3V2NkQ== + dependencies: + "@types/retry" "^0.12.1" + retry "^0.13.1" + p-settle@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/p-settle/-/p-settle-5.0.0.tgz#b7008de2f225ed9132317d995ead4c007684024e" @@ -12296,6 +12529,14 @@ p-settle@^5.0.0: p-limit "^4.0.0" p-reflect "^3.0.0" +p-some@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/p-some/-/p-some-6.0.0.tgz#4613a822038abe125e42152ea9f7705a7967526f" + integrity sha512-CJbQCKdfSX3fIh8/QKgS+9rjm7OBNUTmwWswAFQAhc8j1NR1dsEDETUEuVUtQHZpV+J03LqWBEwvu0g1Yn+TYg== + dependencies: + aggregate-error "^4.0.0" + p-cancelable "^3.0.0" + p-timeout@^3.1.0, p-timeout@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" @@ -13298,6 +13539,27 @@ postgres-interval@^1.1.0: dependencies: xtend "^4.0.0" +prebuild-install@^5.3.3: + version "5.3.6" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.3.6.tgz#7c225568d864c71d89d07f8796042733a3f54291" + integrity sha512-s8Aai8++QQGi4sSbs/M1Qku62PFK49Jm1CbgXklGz4nmHveDq0wzJkg7Na5QbnO1uNH8K7iqx2EQ/mV0MZEmOg== + dependencies: + detect-libc "^1.0.3" + expand-template "^2.0.3" + github-from-package "0.0.0" + minimist "^1.2.3" + mkdirp-classic "^0.5.3" + napi-build-utils "^1.0.1" + node-abi "^2.7.0" + noop-logger "^0.1.1" + npmlog "^4.0.1" + pump "^3.0.0" + rc "^1.2.7" + simple-get "^3.0.3" + tar-fs "^2.0.0" + tunnel-agent "^0.6.0" + which-pm-runs "^1.0.0" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -13338,6 +13600,13 @@ pretty-time@^1.1.0: resolved "https://registry.yarnpkg.com/pretty-time/-/pretty-time-1.1.0.tgz#ffb7429afabb8535c346a34e41873adf3d74dd0e" integrity sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA== +prettycli@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/prettycli/-/prettycli-1.4.3.tgz#b28ec2aad9de07ae1fd75ef294fb54cbdee07ed5" + integrity sha512-KLiwAXXfSWXZqGmZlnKPuGMTFp+0QbcySplL1ft9gfteT/BNsG64Xo8u2Qr9r+qnsIZWBQ66Zs8tg+8s2fmzvw== + dependencies: + chalk "2.1.0" + printj@~1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222" @@ -13628,7 +13897,7 @@ raw-body@2.4.2, raw-body@^2.3.0: iconv-lite "0.4.24" unpipe "1.0.0" -rc@^1.2.8: +rc@^1.2.7, rc@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== @@ -14286,6 +14555,11 @@ resolve-cwd@^3.0.0: dependencies: resolve-from "^5.0.0" +resolve-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + integrity sha1-six699nWiBvItuZTM17rywoYh0g= + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -14864,6 +15138,15 @@ simple-get@^2.7.0: once "^1.3.1" simple-concat "^1.0.0" +simple-get@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-3.1.0.tgz#b45be062435e50d159540b576202ceec40b9c6b3" + integrity sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA== + dependencies: + decompress-response "^4.2.0" + once "^1.3.1" + simple-concat "^1.0.0" + simple-git-hooks@^2.3.1: version "2.7.0" resolved "https://registry.yarnpkg.com/simple-git-hooks/-/simple-git-hooks-2.7.0.tgz#121a5c3023663b8abcc5648c8bfe8619dc263705" @@ -15523,6 +15806,13 @@ supports-color@^2.0.0: resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= +supports-color@^4.0.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" + integrity sha1-vnoN5ITexcXN34s9WRJQRJEvY1s= + dependencies: + has-flag "^2.0.0" + supports-color@^5.3.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -15668,6 +15958,27 @@ tape@^5.2.2: string.prototype.trim "^1.2.5" through "^2.3.8" +tar-fs@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" + integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.1.4" + +tar-stream@^2.1.4: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + temp-dir@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-2.0.0.tgz#bde92b05bdfeb1516e804c9c00ad45177f31321e" @@ -15952,6 +16263,13 @@ tty-browserify@0.0.1: resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.1.tgz#3f05251ee17904dfd0677546670db9651682b811" integrity sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw== +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + tweetnacl-util@^0.15.0: version "0.15.1" resolved "https://registry.yarnpkg.com/tweetnacl-util/-/tweetnacl-util-0.15.1.tgz#b80fcdb5c97bcc508be18c44a4be50f022eea00b" @@ -16904,6 +17222,11 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= +which-pm-runs@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" + integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs= + which-typed-array@^1.1.2: version "1.1.7" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.7.tgz#2761799b9a22d4b8660b3c1b40abaa7739691793"