diff --git a/.changeset/five-fireants-notice.md b/.changeset/five-fireants-notice.md new file mode 100644 index 0000000000000..6e5731e20e2c1 --- /dev/null +++ b/.changeset/five-fireants-notice.md @@ -0,0 +1,5 @@ +--- +'@eth-optimism/common-ts': minor +--- + +Minor upgrade to BaseServiceV2 to expose a full customizable server, instead of just metrics. diff --git a/packages/common-ts/package.json b/packages/common-ts/package.json index 633b3ff070ebb..1336c6235f01f 100644 --- a/packages/common-ts/package.json +++ b/packages/common-ts/package.json @@ -34,21 +34,26 @@ "@eth-optimism/core-utils": "0.8.6", "@sentry/node": "^6.3.1", "bcfg": "^0.1.7", + "body-parser": "^1.20.0", "commander": "^9.0.0", "dotenv": "^16.0.0", "envalid": "^7.2.2", "ethers": "^5.6.8", "express": "^4.17.1", + "express-prom-bundle": "^6.4.1", "lodash": "^4.17.21", + "morgan": "^1.10.0", "pino": "^6.11.3", "pino-multi-stream": "^5.3.0", "pino-sentry": "^0.7.0", - "prom-client": "^13.1.0" + "prom-client": "^13.1.0", + "qs": "^6.10.5" }, "devDependencies": { "@ethersproject/abstract-provider": "^5.6.1", "@ethersproject/abstract-signer": "^5.6.2", - "@types/express": "^4.17.12", + "@types/express": "^4.17.13", + "@types/morgan": "^1.9.3", "@types/pino": "^6.3.6", "@types/pino-multi-stream": "^5.1.1", "chai": "^4.3.4", diff --git a/packages/common-ts/src/base-service/base-service-v2.ts b/packages/common-ts/src/base-service/base-service-v2.ts index c825acbdd538c..c7075c0bbd812 100644 --- a/packages/common-ts/src/base-service/base-service-v2.ts +++ b/packages/common-ts/src/base-service/base-service-v2.ts @@ -6,8 +6,11 @@ import { Command, Option } from 'commander' import { ValidatorSpec, Spec, cleanEnv } from 'envalid' import { sleep } from '@eth-optimism/core-utils' import snakeCase from 'lodash/snakeCase' -import express from 'express' +import express, { Router } from 'express' import prometheus, { Registry } from 'prom-client' +import promBundle from 'express-prom-bundle' +import bodyParser from 'body-parser' +import morgan from 'morgan' import { Logger } from '../common/logger' import { Metric } from './metrics' @@ -19,8 +22,8 @@ export type Options = { export type StandardOptions = { loopIntervalMs?: number - metricsServerPort?: number - metricsServerHostname?: string + port?: number + hostname?: string } export type OptionsSpec = { @@ -43,6 +46,8 @@ export type MetricsSpec = { } } +export type ExpressRouter = Router + /** * BaseServiceV2 is an advanced but simple base class for long-running TypeScript services. */ @@ -71,6 +76,11 @@ export abstract class BaseServiceV2< */ protected done: boolean + /** + * Whether or not the service is currently healthy. + */ + protected healthy: boolean + /** * Logger class for this service. */ @@ -97,19 +107,19 @@ export abstract class BaseServiceV2< protected readonly metricsRegistry: Registry /** - * Metrics server. + * App server. */ - protected metricsServer: Server + protected server: Server /** - * Port for the metrics server. + * Port for the app server. */ - protected readonly metricsServerPort: number + protected readonly port: number /** - * Hostname for the metrics server. + * Hostname for the app server. */ - protected readonly metricsServerHostname: string + protected readonly hostname: string /** * @param params Options for the construction of the service. @@ -122,8 +132,8 @@ export abstract class BaseServiceV2< * @param params.options Options to pass to the service. * @param params.loops Whether or not the service should loop. Defaults to true. * @param params.loopIntervalMs Loop interval in milliseconds. Defaults to zero. - * @param params.metricsServerPort Port for the metrics server. Defaults to 7300. - * @param params.metricsServerHostname Hostname for the metrics server. Defaults to 0.0.0.0. + * @param params.port Port for the app server. Defaults to 7300. + * @param params.hostname Hostname for the app server. Defaults to 0.0.0.0. */ constructor(params: { name: string @@ -132,8 +142,8 @@ export abstract class BaseServiceV2< options?: Partial loop?: boolean loopIntervalMs?: number - metricsServerPort?: number - metricsServerHostname?: string + port?: number + hostname?: string }) { this.loop = params.loop !== undefined ? params.loop : true this.state = {} as TServiceState @@ -148,15 +158,15 @@ export abstract class BaseServiceV2< desc: 'Loop interval in milliseconds', default: params.loopIntervalMs || 0, }, - metricsServerPort: { + port: { validator: validators.num, - desc: 'Port for the metrics server', - default: params.metricsServerPort || 7300, + desc: 'Port for the app server', + default: params.port || 7300, }, - metricsServerHostname: { + hostname: { validator: validators.str, - desc: 'Hostname for the metrics server', - default: params.metricsServerHostname || '0.0.0.0', + desc: 'Hostname for the app server', + default: params.hostname || '0.0.0.0', }, } @@ -268,12 +278,13 @@ export abstract class BaseServiceV2< // Create the metrics server. this.metricsRegistry = prometheus.register - this.metricsServerPort = this.options.metricsServerPort - this.metricsServerHostname = this.options.metricsServerHostname + this.port = this.options.port + this.hostname = this.options.hostname // Set up everything else. this.loopIntervalMs = this.options.loopIntervalMs this.logger = new Logger({ name: params.name }) + this.healthy = true // Gracefully handle stop signals. const maxSignalCount = 3 @@ -307,30 +318,69 @@ export abstract class BaseServiceV2< public async run(): Promise { this.done = false - // Start the metrics server if not yet running. - if (!this.metricsServer) { - this.logger.info('starting metrics server') + // Start the app server if not yet running. + if (!this.server) { + this.logger.info('starting app server') + + // Start building the app. + const app = express() + + // Body parsing. + app.use(bodyParser.json()) + app.use(bodyParser.urlencoded({ extended: true })) + + // Logging. + app.use( + morgan((tokens, req, res) => { + return [ + tokens.method(req, res), + tokens.url(req, res), + tokens.status(req, res), + JSON.stringify(req.body), + '\n', + tokens.res(req, res, 'content-length'), + '-', + tokens['response-time'](req, res), + 'ms', + ].join(' ') + }) + ) - await new Promise((resolve) => { - const app = express() + // Metrics. + // Will expose a /metrics endpoint by default. + app.use( + promBundle({ + promRegistry: this.metricsRegistry, + includeMethod: true, + includePath: true, + includeStatusCode: true, + }) + ) - app.get('/metrics', async (_, res) => { - res.status(200).send(await this.metricsRegistry.metrics()) + // Health status. + app.get('/healthz', async (req, res) => { + return res.json({ + ok: this.healthy, }) + }) - this.metricsServer = app.listen( - this.metricsServerPort, - this.metricsServerHostname, - () => { - resolve(null) - } - ) + // Registery user routes. + if (this.routes) { + const router = express.Router() + this.routes(router) + app.use('/api', router) + } + + // Wait for server to come up. + await new Promise((resolve) => { + this.server = app.listen(this.port, this.hostname, () => { + resolve(null) + }) }) - this.logger.info(`metrics started`, { - port: this.metricsServerPort, - hostname: this.metricsServerHostname, - route: '/metrics', + this.logger.info(`app server started`, { + port: this.port, + hostname: this.hostname, }) } @@ -381,15 +431,15 @@ export abstract class BaseServiceV2< } // Shut down the metrics server if it's running. - if (this.metricsServer) { + if (this.server) { this.logger.info('stopping metrics server') await new Promise((resolve) => { - this.metricsServer.close(() => { + this.server.close(() => { resolve(null) }) }) this.logger.info('metrics server stopped') - this.metricsServer = undefined + this.server = undefined } } @@ -398,6 +448,13 @@ export abstract class BaseServiceV2< */ protected init?(): Promise + /** + * Initialization function for router. + * + * @param router Express router. + */ + protected routes?(router: ExpressRouter): Promise + /** * Main function. Runs repeatedly when run() is called. */ diff --git a/yarn.lock b/yarn.lock index 53832caaab8ca..0637d1b1e696a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3295,7 +3295,7 @@ "@types/qs" "*" "@types/range-parser" "*" -"@types/express@^4.17.12": +"@types/express@^4.17.12", "@types/express@^4.17.13": version "4.17.13" resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== @@ -3398,6 +3398,13 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4" integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== +"@types/morgan@^1.9.3": + version "1.9.3" + resolved "https://registry.yarnpkg.com/@types/morgan/-/morgan-1.9.3.tgz#ae04180dff02c437312bc0cfb1e2960086b2f540" + integrity sha512-BiLcfVqGBZCyNCnCH3F4o2GmDLrpy0HeBVnNlyZG4fo88ZiE9SoiBe3C+2ezuwbjlEyT+PDZ17//TAlRxAn75Q== + dependencies: + "@types/node" "*" + "@types/node-fetch@^2.5.5": version "2.5.10" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.10.tgz#9b4d4a0425562f9fcea70b12cb3fcdd946ca8132" @@ -5062,6 +5069,13 @@ base@^0.11.1: mixin-deep "^1.2.0" pascalcase "^0.1.1" +basic-auth@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + bcfg@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/bcfg/-/bcfg-0.1.6.tgz#f77a6323bddef14f3886222e7ef8ccc0bc2143ec" @@ -5215,6 +5229,24 @@ body-parser@1.19.0, body-parser@^1.16.0: raw-body "2.4.0" type-is "~1.6.17" +body-parser@^1.20.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5" + integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.10.3" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + boundary@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/boundary/-/boundary-1.0.1.tgz#4d67dc2602c0cc16dd9bce7ebf87e948290f5812" @@ -5454,6 +5486,11 @@ bytes@3.1.0: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + bytewise-core@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/bytewise-core/-/bytewise-core-1.2.3.tgz#3fb410c7e91558eb1ab22a82834577aa6bd61d42" @@ -6859,6 +6896,11 @@ depcheck@^1.4.3: semver "^7.3.2" yargs "^16.1.0" +depd@2.0.0, depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + depd@^1.1.2, depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" @@ -6882,6 +6924,11 @@ des.js@^1.0.0: inherits "^2.0.1" minimalistic-assert "^1.0.0" +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + destroy@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" @@ -8475,7 +8522,7 @@ expand-template@^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-prom-bundle@^6.3.6: +express-prom-bundle@^6.3.6, express-prom-bundle@^6.4.1: version "6.4.1" resolved "https://registry.yarnpkg.com/express-prom-bundle/-/express-prom-bundle-6.4.1.tgz#a688050b9e090f6969825c33143106d3e0e5a70e" integrity sha512-Sg0svLQe/SS5z1tHDTVfZVjNumobiDlXM0jmemt5Dm9K6BX8z9yCwEr93zbko6fNMR4zKav77iPfxUWi6gAjNA== @@ -9950,6 +9997,17 @@ http-errors@1.7.3, http-errors@~1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + http-https@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/http-https/-/http-https-1.0.0.tgz#2f908dd5f1db4068c058cd6e6d4ce392c913389b" @@ -12583,6 +12641,17 @@ modify-values@^1.0.0: resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== +morgan@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" + integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== + dependencies: + basic-auth "~2.0.1" + debug "2.6.9" + depd "~2.0.0" + on-finished "~2.3.0" + on-headers "~1.0.2" + mri@1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.4.tgz#7cb1dd1b9b40905f1fac053abe25b6720f44744a" @@ -13265,6 +13334,13 @@ oboe@2.1.5: dependencies: http-https "^1.0.0" +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + on-finished@^2.3.0, on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -13272,6 +13348,11 @@ on-finished@^2.3.0, on-finished@~2.3.0: dependencies: ee-first "1.1.1" +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + once@1.x, once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -14261,11 +14342,25 @@ q@^1.5.1: resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= +qs@6.10.3: + version "6.10.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" + integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ== + dependencies: + side-channel "^1.0.4" + qs@6.7.0: version "6.7.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== +qs@^6.10.5: + version "6.10.5" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.5.tgz#974715920a80ff6a262264acd2c7e6c2a53282b4" + integrity sha512-O5RlPh0VFtR78y79rgcgKK4wbAI0C5zGVLztOIdpWX6ep368q5Hv6XRxDvXuZ9q3C6v+e3n8UfZZJw7IIG27eQ== + dependencies: + side-channel "^1.0.4" + qs@^6.4.0, qs@^6.7.0, qs@^6.9.4: version "6.10.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a" @@ -14364,6 +14459,16 @@ raw-body@2.4.0: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + raw-body@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c" @@ -15315,6 +15420,11 @@ setprototypeof@1.1.1: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + sha.js@^2.4.0, sha.js@^2.4.8: version "2.4.11" resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" @@ -15890,6 +16000,11 @@ static-extend@^0.1.1: define-property "^0.2.5" object-copy "^0.1.0" +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + "statuses@>= 1.5.0 < 2", statuses@~1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" @@ -16584,6 +16699,11 @@ toidentifier@1.0.0: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + tough-cookie@^2.3.3, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2"