Improve parkio, add domainrproxy

This commit is contained in:
2023-09-18 17:39:14 -06:00
parent 6aed764160
commit 3e3b1c46fc
13 changed files with 327 additions and 43 deletions

View File

@@ -20,6 +20,7 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/cache-manager": "^2.1.0",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.0.0",
@@ -28,6 +29,8 @@
"@nestjs/swagger": "^7.1.11",
"axios": "^1.5.0",
"bcrypt": "^5.1.1",
"cache-manager": "^5.2.3",
"hbs": "^4.2.0",
"minio": "^7.1.3",
"ramda": "^0.29.0",
"reflect-metadata": "^0.1.13",

View File

@@ -6,18 +6,22 @@ import { IswordModule } from './isword/isword.module';
import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module';
import { ConfigModule } from '@nestjs/config';
import { DomainrproxyModule } from './domainrproxy/domainrproxy.module';
import configuration from './config/configuration';
import { CacheModule } from '@nestjs/cache-manager';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [configuration]
load: [configuration],
}),
CacheModule.register({ isGlobal: true }),
ParkioModule,
IswordModule,
AuthModule,
UsersModule,
DomainrproxyModule,
],
controllers: [AppController],
providers: [AppService],

View File

@@ -3,6 +3,6 @@ import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
return "Hello World! This is Chip's generalized API for fetching information and things. You can contact me on mastodon @chip@talking.dev.";
}
}

View File

@@ -10,6 +10,7 @@ import {
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthGuard } from './auth.guard';
import { ApiBearerAuth } from '@nestjs/swagger';
@Controller('auth')
export class AuthController {
@@ -28,6 +29,7 @@ export class AuthController {
}
@UseGuards(AuthGuard)
@ApiBearerAuth()
@Get('profile')
getProfile(@Request() req: any) {
return req.user;

View File

@@ -1,3 +1,6 @@
export default () => ({
port: parseInt(process.env.PORT ?? '', 10) || 3000,
// UserAgent should be added to calls made to third party apis
userAgent: 'api.us.dev/@chip@talking.dev',
rapidApiKey: process.env.RAPID_API_KEY || '',
});

View File

@@ -0,0 +1,20 @@
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
import { DomainrproxyService } from './domainrproxy.service';
import { AuthGuard } from 'src/auth/auth.guard';
@Controller('domainrproxy')
export class DomainrproxyController {
constructor(private readonly proxyService: DomainrproxyService) {}
@UseGuards(AuthGuard)
@Get(':domain')
queryDomain(@Param('domain') domain: string) {
return this.proxyService.queryForDomain(domain);
}
@UseGuards(AuthGuard)
@Post('search')
search(@Body() body: { query: string }) {
return this.proxyService.search(body.query);
}
}

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { DomainrproxyService } from './domainrproxy.service';
import { DomainrproxyController } from './domainrproxy.controller';
@Module({
providers: [DomainrproxyService],
controllers: [DomainrproxyController]
})
export class DomainrproxyModule {}

View File

@@ -0,0 +1,48 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import axios from 'axios';
@Injectable()
export class DomainrproxyService {
private readonly logger = new Logger(DomainrproxyService.name);
constructor(private readonly configService: ConfigService) {}
queryForDomain = async (domain: string) => {
this.logger.verbose(`Handling domainr query for domain ${domain}`);
const options = {
method: 'GET',
url: 'https://domainr.p.rapidapi.com/v2/status',
params: {
domain,
'mashape-key': this.configService.get<string>('rapidApiKey'),
},
headers: {
'X-RapidAPI-Key': this.configService.get<string>('rapidApiKey'),
'X-RapidAPI-Host': 'domainr.p.rapidapi.com',
},
};
const result = await axios.request(options);
return result.data;
};
search = async (query: string) => {
this.logger.verbose(`Handling domainr search for term ${query}`);
const options = {
method: 'GET',
url: 'https://domainr.p.rapidapi.com/v2/search',
params: {
query,
defaults: 'com,net,org,io,sh,wtf',
'mashape-key': this.configService.get<string>('rapidApiKey'),
},
headers: {
'X-RapidAPI-Key': this.configService.get<string>('rapidApiKey'),
'X-RapidAPI-Host': 'domainr.p.rapidapi.com',
},
};
const result = await axios.request(options);
return result.data;
};
}

View File

@@ -1,13 +1,19 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join } from 'path';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.useStaticAssets(join(__dirname, '..', 'public'));
app.setBaseViewsDir(join(__dirname, '..', 'views'));
app.setViewEngine('hbs');
const config = new DocumentBuilder()
.setTitle('us.dev API')
.setDescription("Chip's generalized API")
.setVersion('1.0')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);

View File

@@ -1,13 +1,12 @@
import { Controller, Get, Param } from '@nestjs/common';
import { Controller, Get, Param, Render, Res } from '@nestjs/common';
import { ParkioService } from './parkio.service';
import { ApiTags } from '@nestjs/swagger';
import { ParsedDomain } from './types';
@ApiTags('parkio')
@Controller('parkio')
export class ParkioController {
constructor(
private readonly parkioService: ParkioService
) {}
constructor(private readonly parkioService: ParkioService) {}
@Get('auctions')
async getAuctions() {
@@ -15,7 +14,7 @@ export class ParkioController {
}
@Get('domains')
async getDomains () {
async getDomains(): Promise<ParsedDomain[]> {
return this.parkioService.fetchAllDomains();
}
@@ -24,12 +23,25 @@ export class ParkioController {
return {
maxLength,
domains: (await this.parkioService.fetchAllDomains()).filter(
parsedDomain => parsedDomain.domain_length <= maxLength
(parsedDomain) => parsedDomain.domain_length <= maxLength,
),
auctions: (await this.parkioService.fetchAuctions()).filter(
parsedAuction => parsedAuction.domain_length <= maxLength
(parsedAuction) => parsedAuction.domain_length <= maxLength,
),
};
}
@Get('report')
@Render('parkio/dailyreport')
async getReport() {
const source = {
auctions: await this.parkioService.fetchAuctions(),
domains: await this.parkioService.fetchAllDomains(),
};
return {
...source,
day: new Date().toLocaleDateString(),
wordDomains: source.domains.filter((domain) => domain.is_word),
};
}
}

View File

@@ -1,51 +1,124 @@
import { Injectable } from '@nestjs/common';
import { Inject, Injectable, Logger } from '@nestjs/common';
import {
Auction,
AuctionsResponse,
Domain,
DomainSet,
DomainsResponse,
ParkioTld,
ParsedAuction,
ParsedDomain,
TLDs,
} from './types';
import axios from 'axios';
import { filter, map, pipe } from 'ramda';
import { IswordService } from 'src/isword/isword.service';
import { ConfigService } from '@nestjs/config';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
const parkIoEndpoints = {
domains: 'https://park.io/domains.json',
auctions: 'https://park.io/auctions.json',
tld: (tld: ParkioTld) => `https://park.io/domains/index/${tld}.json`,
tld: (tld: ParkioTld, limit: number = 80_000, page: number = 1) =>
`https://park.io/domains/index/${tld}/page:${page}.json?limit=${limit}`,
all: (limit: number = 10_000) =>
`https://park.io/domains/index/all.json?limit=${limit}`,
};
@Injectable()
export class ParkioService {
private readonly logger = new Logger(ParkioService.name);
constructor(
private readonly isWordService: IswordService
private configService: ConfigService,
private readonly isWordService: IswordService,
@Inject(CACHE_MANAGER) private cacheManager: Cache,
) {}
fetchDomainsForTld = async (tld: ParkioTld) => {};
tldRawCacheKey = (tld: ParkioTld) => `${tld}_raw`;
fetchAllDomains = async (limit: number = 10_000): Promise<ParsedDomain[]> => {
let response = await axios.get<DomainsResponse>(parkIoEndpoints.all(limit));
setTldDomainsCache = async (
tld: ParkioTld,
domains: ParsedDomain[],
): Promise<void> =>
this.cacheManager.set(this.tldRawCacheKey(tld), domains, 300_000);
// There may have been more than our set limit
if (response.data.current === limit) {
return await this.fetchAllDomains(limit + 200);
getTldDomainsCache = (tld: ParkioTld): Promise<ParsedDomain[] | undefined> =>
this.cacheManager.get<ParsedDomain[]>(this.tldRawCacheKey(tld));
fetchDomainsForTld = async (tld: ParkioTld): Promise<ParsedDomain[]> => {
this.logger.verbose(`Fetching domains for ${tld}`);
const cacheValue = await this.getTldDomainsCache(tld);
if (cacheValue !== undefined) {
this.logger.verbose(`${tld} ... cache hit`);
return cacheValue;
}
return map(this.parseDomain, response.data.domains);
this.logger.verbose(`${tld} ... cache miss`);
// Recursively fetch domain
const results = await this.fetchDomainPageForTld(tld);
// Fire the combined results into the cache
this.setTldDomainsCache(tld, results);
return results;
};
fetchDomainPageForTld = async (
tld: ParkioTld,
pageLimit: number = 1_000,
page: number = 1,
): Promise<ParsedDomain[]> => {
this.logger.verbose(`Fetching ${pageLimit} .${tld} domains, page ${page}`);
const url = parkIoEndpoints.tld(tld, pageLimit, page);
const response = await axios.get<DomainsResponse>(url, {
headers: {
Accept: 'application/json',
'User-Agent': this.configService.get<string>('userAgent'),
},
});
// There may have been more than our set limit
if (response.data.nextPage) {
return [
...response.data.domains.map(this.parseDomain),
...(await this.fetchDomainPageForTld(tld, pageLimit, page + 1)),
];
}
return response.data.domains.map(this.parseDomain);
};
fetchAllDomains = async (): Promise<ParsedDomain[]> => {
const domainResults = await Promise.all(TLDs.map(this.fetchDomainsForTld));
return domainResults.reduce((acc, domainResponse) => [
...acc,
...domainResponse,
]);
};
auctionsRawCacheKey = `auctions_raw`;
setAuctionsCache = async (auctions: ParsedAuction[]): Promise<void> =>
this.cacheManager.set(this.auctionsRawCacheKey, auctions);
getAuctionsCache = async (): Promise<ParsedAuction[] | undefined> =>
this.cacheManager.get<ParsedAuction[]>(this.auctionsRawCacheKey);
fetchAuctions = async (): Promise<ParsedAuction[]> => {
this.logger.verbose('Fetching auction data');
const cachedValue = await this.getAuctionsCache();
if (cachedValue !== undefined) {
this.logger.verbose('... cache hit');
return cachedValue;
}
this.logger.verbose('... cache miss');
const response = await axios.get<AuctionsResponse>(
parkIoEndpoints.auctions,
);
return map(this.parseAuction, response.data.auctions);
this.logger.verbose('Processing auction response');
const results = map(this.parseAuction, response.data.auctions);
this.setAuctionsCache(results);
return results;
};
lengthFilter = (length: number) => (parsedDomain: ParsedDomain) =>
@@ -80,10 +153,12 @@ export class ParkioService {
date_registered: domain.date_registered
? this.parseDate(domain.date_registered)
: undefined,
is_word: this.isWordService.isWord(domainName)
is_word: this.isWordService.isWord(domainName),
};
};
parseDomains = map(this.parseDomain);
parseDate = (date: string) =>
new Date(
Date.parse(

View File

@@ -0,0 +1,37 @@
<html>
<head>
<meta charset='utf-8' />
<title>Daily park.io Report</title>
</head>
<body>
<h2>Daily <a href='https://park.io'>park.io</a> report for {{day}}</h2>
<table>
<tr>
<th>Name</th>
<th>Current Bid</th>
<th>Bid Count</th>
<th>Closes</th>
</tr>
{{#each auctions}}
<tr>
<td><a href='https://park.io/auctions/view/{{name}}'>{{name}}</a></td>
<td>${{price}}</td>
<td>{{num_bids}}</td>
<td>{{close_date}}</td>
</tr>
{{/each}}
</table>
<table>
<tr>
<th>Domain</th>
<th>TLD</th>
</tr>
{{#each wordDomains}}
<tr>
<td><a href='https://park.io/auctions/view/{{name}}'>{{name}}</a></td>
<td>{{tld}}</td>
</tr>
{{/each}}
</table>
</body>
</html>

View File

@@ -697,6 +697,11 @@
semver "^7.3.5"
tar "^6.1.11"
"@nestjs/cache-manager@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@nestjs/cache-manager/-/cache-manager-2.1.0.tgz#e4dadc4ba9c02c059db4dac5e0b5513466e2895a"
integrity sha512-9kep3a8Mq5cMuXN/anGhSYc0P48CRBXk5wyJJRBFxhNkCH8AIzZF4CASGVDIEMmm3OjVcEUHojjyJwCODS17Qw==
"@nestjs/cli@^10.0.0":
version "10.1.17"
resolved "https://registry.yarnpkg.com/@nestjs/cli/-/cli-10.1.17.tgz#c7e90e443e0967be2b12dc912957f89ed4b5c992"
@@ -1796,6 +1801,14 @@ bytes@3.1.2:
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
cache-manager@^5.2.3:
version "5.2.3"
resolved "https://registry.yarnpkg.com/cache-manager/-/cache-manager-5.2.3.tgz#b6a8b4469c57fdfdae1deed7f81ea9e057c7eade"
integrity sha512-9OErI8fksFkxAMJ8Mco0aiZSdphyd90HcKiOMJQncSlU1yq/9lHHxrT8PDayxrmr9IIIZPOAEfXuGSD7g29uog==
dependencies:
lodash.clonedeep "^4.5.0"
lru-cache "^9.1.2"
call-bind@^1.0.0, call-bind@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
@@ -2693,6 +2706,11 @@ for-each@^0.3.3:
dependencies:
is-callable "^1.1.3"
foreachasync@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/foreachasync/-/foreachasync-3.0.0.tgz#5502987dc8714be3392097f32e0071c9dee07cf6"
integrity sha512-J+ler7Ta54FwwNcx6wQRDhTIbNeyDcARMkOcguEqnEdtm0jKvN3Li3PDAb2Du3ubJYEWfYL83XMROXdsXAXycw==
fork-ts-checker-webpack-plugin@8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz#dae45dfe7298aa5d553e2580096ced79b6179504"
@@ -2910,6 +2928,18 @@ graphemer@^1.4.0:
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
handlebars@4.7.7:
version "4.7.7"
resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.7.tgz#9ce33416aad02dbd6c8fafa8240d5d98004945a1"
integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==
dependencies:
minimist "^1.2.5"
neo-async "^2.6.0"
source-map "^0.6.1"
wordwrap "^1.0.0"
optionalDependencies:
uglify-js "^3.1.4"
has-flag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
@@ -2954,6 +2984,14 @@ has@^1.0.3:
dependencies:
function-bind "^1.1.1"
hbs@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/hbs/-/hbs-4.2.0.tgz#10e40dcc24d5be7342df9636316896617542a32b"
integrity sha512-dQwHnrfWlTk5PvG9+a45GYpg0VpX47ryKF8dULVd6DtwOE6TEcYQXQ5QM6nyOx/h7v3bvEQbdn19EDAcfUAgZg==
dependencies:
handlebars "4.7.7"
walk "2.3.15"
hexoid@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18"
@@ -3814,6 +3852,11 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
lodash.clonedeep@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==
lodash.memoize@4.x:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@@ -3856,6 +3899,11 @@ lru-cache@^6.0.0:
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.1.tgz#0a3be479df549cca0e5d693ac402ff19537a6b7a"
integrity sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==
lru-cache@^9.1.2:
version "9.1.2"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-9.1.2.tgz#255fdbc14b75589d6d0e73644ca167a8db506835"
integrity sha512-ERJq3FOzJTxBbFjZ7iDs+NiK4VI9Wz+RdrrAB8dio1oV+YvdPzUEE4QNiT2VD51DkIbCYRUUzCRkssXCHqSnKQ==
macos-release@^2.5.0:
version "2.5.1"
resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.5.1.tgz#bccac4a8f7b93163a8d163b8ebf385b3c5f55bf9"
@@ -3987,7 +4035,7 @@ minimatch@^8.0.2:
dependencies:
brace-expansion "^2.0.1"
minimist@^1.2.6:
minimist@^1.2.5, minimist@^1.2.6:
version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
@@ -4097,7 +4145,7 @@ negotiator@0.6.3:
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
neo-async@^2.6.2:
neo-async@^2.6.0, neo-async@^2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
@@ -5233,6 +5281,11 @@ typescript@^5.1.3:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"
integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==
uglify-js@^3.1.4:
version "3.17.4"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c"
integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==
uid@2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/uid/-/uid-2.0.2.tgz#4b5782abf0f2feeefc00fa88006b2b3b7af3e3b9"
@@ -5315,6 +5368,13 @@ vary@^1, vary@~1.1.2:
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
walk@2.3.15:
version "2.3.15"
resolved "https://registry.yarnpkg.com/walk/-/walk-2.3.15.tgz#1b4611e959d656426bc521e2da5db3acecae2424"
integrity sha512-4eRTBZljBfIISK1Vnt69Gvr2w/wc3U6Vtrw7qiN5iqYJPH7LElcYh/iU4XWhdCy2dZqv1ToMyYlybDylfG/5Vg==
dependencies:
foreachasync "^3.0.0"
walker@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f"
@@ -5431,6 +5491,11 @@ windows-release@^4.0.0:
dependencies:
execa "^4.0.2"
wordwrap@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
wrap-ansi@^6.0.1:
version "6.2.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"