This commit is contained in:
Sergey Krylov 2025-07-05 09:47:38 +03:00
parent 7de0962ed6
commit 7724bc460f
21 changed files with 337 additions and 14 deletions

View File

@ -35,6 +35,7 @@
"@react-email/components": "^0.1.1",
"@react-email/html": "^0.0.11",
"@react-email/tailwind": "^1.0.5",
"@types/qrcode": "^1.5.5",
"argon2": "^0.43.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
@ -44,9 +45,12 @@
"express-session": "^1.18.1",
"geoip-lite": "^1.4.10",
"graphql": "^16.11.0",
"hi-base32": "^0.5.1",
"i18n-iso-countries": "^7.14.0",
"ioredis": "^5.6.1",
"otpauth": "^9.4.0",
"prisma": "^6.10.1",
"qrcode": "^1.5.4",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"reflect-metadata": "^0.2.2",

View File

@ -19,6 +19,8 @@ model User {
token Token[]
isVerified Boolean @default(false) @map("is_verified")
isEmailVerified Boolean @default(false) @map("is_email_verified")
isTotpEnabled Boolean @default(false) @map("is_totp_enabled")
totpSecret String? @map("totp_secret")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

View File

@ -2,6 +2,7 @@ import { getGraphQLConfig } from '@/src/core/config/graphql.config'
import { AccountModule } from '@/src/module/auth/account/account.module'
import { PasswordRecoveryModule } from '@/src/module/auth/password-recovery/password-recovery.module'
import { SessionModule } from '@/src/module/auth/session/session.module'
import { TotpModule } from '@/src/module/auth/totp/totp.module'
import { VerificationModule } from '@/src/module/auth/verification/verification.module'
import { MailModule } from '@/src/module/libs/mail/mail.module'
import { IS_DEV } from '@/src/shared/util/is-dev.util'
@ -31,6 +32,7 @@ import { RedisModule } from './redis/redis.module'
SessionModule,
VerificationModule,
PasswordRecoveryModule,
TotpModule,
],
})
export class CoreModule {}

View File

@ -2,6 +2,11 @@
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------
type AuthModel {
message: String
user: UserModel
}
input CreateUserInput {
email: String!
name: String!
@ -19,6 +24,11 @@ type DeviceModel {
type: String!
}
input EnableTotpInput {
pin: String!
secret: String!
}
type LocationModel {
city: String!
country: String!
@ -29,12 +39,15 @@ type LocationModel {
input LoginInput {
login: String!
password: String!
pin: String
}
type Mutation {
clearSessionCookie: Boolean!
createUser(data: CreateUserInput!): UserModel!
loginUser(data: LoginInput!): UserModel!
disableTotp: Boolean!
enableTotp(data: EnableTotpInput!): Boolean!
loginUser(data: LoginInput!): AuthModel!
logoutUser: Boolean!
removeSession(id: String!): Boolean!
resetPassword(data: ResetPasswordInput!): Boolean!
@ -52,6 +65,7 @@ type Query {
findCurrentSession: SessionModel!
findProfile: UserModel!
findSessionsByUser: [SessionModel!]!
generateTotpSecret: TotpModel!
}
input ResetPasswordInput {
@ -71,6 +85,11 @@ type SessionModel {
userId: String!
}
type TotpModel {
qrcodeUrl: String!
secret: String!
}
type UserModel {
avatar: String
bio: String
@ -79,9 +98,11 @@ type UserModel {
email: String!
id: ID!
isEmailVerified: Boolean!
isTotpEnabled: Boolean!
isVerified: Boolean!
name: String!
password: String!
totpSecret: String
updatedAt: DateTime!
}

View File

@ -0,0 +1,11 @@
import { UserModel } from '@/src/module/auth/account/models/user.model'
import { Field, ObjectType } from '@nestjs/graphql'
@ObjectType()
export class AuthModel {
@Field(() => UserModel, { nullable: true })
public user?: UserModel
@Field(() => String, { nullable: true })
public message: string
}

View File

@ -30,6 +30,12 @@ export class UserModel implements User {
@Field(() => Boolean)
isVerified: boolean
@Field(() => Boolean)
isTotpEnabled: boolean
@Field(() => String, { nullable: true })
totpSecret: string
@Field(() => Date)
createdAt: Date

View File

@ -1,5 +1,5 @@
import { NewPasswordInput } from '@/src/module/auth/password-recovery/input/new-password.input'
import { ResetPasswordInput } from '@/src/module/auth/password-recovery/input/reset-password.input'
import { NewPasswordInput } from '@/src/module/auth/password-recovery/inputs/new-password.input'
import { ResetPasswordInput } from '@/src/module/auth/password-recovery/inputs/reset-password.input'
import { UserAgent } from '@/src/shared/decorators/user-agent.decorator'
import { GqlContext } from '@/src/shared/types/gql-context.types'
import { Args, Context, Mutation, Resolver } from '@nestjs/graphql'

View File

@ -1,6 +1,6 @@
import { PrismaService } from '@/src/core/prisma/prisma.service'
import { NewPasswordInput } from '@/src/module/auth/password-recovery/input/new-password.input'
import { ResetPasswordInput } from '@/src/module/auth/password-recovery/input/reset-password.input'
import { NewPasswordInput } from '@/src/module/auth/password-recovery/inputs/new-password.input'
import { ResetPasswordInput } from '@/src/module/auth/password-recovery/inputs/reset-password.input'
import { MailService } from '@/src/module/libs/mail/mail.service'
import { generateToken } from '@/src/shared/util/generate-token.util'
import { getSessionMetadata } from '@/src/shared/util/session-metadata.util'

View File

@ -1,5 +1,5 @@
import { Field, InputType } from '@nestjs/graphql'
import { IsNotEmpty, IsString, MinLength } from 'class-validator'
import { IsNotEmpty, IsOptional, IsString, Length, MinLength } from 'class-validator'
@InputType()
export class LoginInput {
@ -13,4 +13,11 @@ export class LoginInput {
@IsNotEmpty()
@MinLength(8)
password: string
@Field(() => String, { nullable: true })
@IsString()
@IsNotEmpty()
@IsOptional()
@Length(6, 6)
pin?: string
}

View File

@ -1,3 +1,4 @@
import { AuthModel } from '@/src/module/auth/account/models/auth.model';
import { UserModel } from '@/src/module/auth/account/models/user.model'
import { LoginInput } from '@/src/module/auth/session/inputs/login.input'
import { Authorization } from '@/src/shared/decorators/auth.decorator'
@ -27,7 +28,7 @@ export class SessionResolver {
return this.sessionService.findCurrentSession(req)
}
@Mutation(() => UserModel, { name: 'loginUser' })
@Mutation(() => AuthModel, { name: 'loginUser' })
public async login(
@Context() { req }: GqlContext,
@Args('data') input: LoginInput,

View File

@ -15,6 +15,7 @@ import { ConfigService } from '@nestjs/config'
import { verify } from 'argon2'
import { Request } from 'express'
import { SessionData } from 'express-session'
import { TOTP } from 'otpauth'
@Injectable()
export class SessionService {
@ -74,7 +75,7 @@ export class SessionService {
}
public async login(req: Request, input: LoginInput, userAgent: string) {
const { login, password } = input
const { login, password, pin } = input
const user = await this.prismaService.user.findFirst({ where: {
OR: [
@ -97,6 +98,28 @@ export class SessionService {
throw new BadRequestException('Аккаунт не верифицирован. Проверьте свою почту для подтверждения')
}
if (user.isTotpEnabled) {
if (!pin) {
return {
message: 'Необходимо ввести пин-код для завершения операции',
}
}
const totp = new TOTP({
issuer: 'TeaStream',
label: user.email,
algorithm: 'SHA-1',
digits: 6,
secret: user.totpSecret!,
})
const delta = totp.validate({ token: pin })
if (delta === null) {
throw new BadRequestException('Невереый код')
}
}
return saveSession(req, user, getSessionMetadata(req, userAgent))
}

View File

@ -0,0 +1,16 @@
import { Field, InputType } from '@nestjs/graphql'
import { IsNotEmpty, IsString, Length } from 'class-validator'
@InputType()
export class EnableTotpInput {
@Field(() => String)
@IsString()
@IsNotEmpty()
secret: string
@Field(() => String)
@IsString()
@IsNotEmpty()
@Length(6, 6)
pin: string
}

View File

@ -0,0 +1,10 @@
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class TotpModel {
@Field(() => String)
public qrcodeUrl: string
@Field(() => String)
public secret: string
}

View File

@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { TotpService } from './totp.service';
import { TotpResolver } from './totp.resolver';
@Module({
providers: [TotpResolver, TotpService],
})
export class TotpModule {}

View File

@ -0,0 +1,33 @@
import { EnableTotpInput } from '@/src/module/auth/totp/inputs/enable-totp.input'
import { TotpModel } from '@/src/module/auth/totp/models/totp.model'
import { Authorization } from '@/src/shared/decorators/auth.decorator'
import { Authorized } from '@/src/shared/decorators/authorized.decorator'
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'
import { User } from '@prisma/generated'
import { TotpService } from './totp.service'
@Resolver('Totp')
export class TotpResolver {
constructor(private readonly totpService: TotpService) {}
@Authorization()
@Query(() => TotpModel, { name: 'generateTotpSecret' })
public async generate(@Authorized() user: User) {
return this.totpService.generate(user)
}
@Authorization()
@Mutation(() => Boolean, { name: 'enableTotp' })
public async enable(
@Authorized() user: User,
@Args('data') input: EnableTotpInput,
) {
return this.totpService.enable(user, input)
}
@Authorization()
@Mutation(() => Boolean, { name: 'disableTotp' })
public async disable(@Authorized() user: User) {
return this.totpService.disable(user)
}
}

View File

@ -0,0 +1,77 @@
import { PrismaService } from '@/src/core/prisma/prisma.service'
import { EnableTotpInput } from '@/src/module/auth/totp/inputs/enable-totp.input'
import { BadRequestException, Injectable } from '@nestjs/common'
import { User } from '@prisma/generated'
import { encode } from 'hi-base32'
import { randomBytes } from 'node:crypto'
import { TOTP } from 'otpauth'
import * as QRCode from 'qrcode'
@Injectable()
export class TotpService {
constructor(
private readonly prismaService: PrismaService,
) {
}
async generate(user: User) {
const secret = encode(randomBytes(15))
.replace(/=/g, '')
.substring(0, 24)
const totp = new TOTP({
issuer: 'TeaStream',
label: user.email,
algorithm: 'SHA-1',
digits: 6,
secret,
})
const qrcodeUrl = await QRCode.toDataURL(totp.toString())
return {
qrcodeUrl,
secret,
}
}
async enable(user: User, input: EnableTotpInput) {
const { pin, secret } = input
const totp = new TOTP({
issuer: 'TeaStream',
label: user.email,
algorithm: 'SHA-1',
digits: 6,
secret,
})
const delta = totp.validate({ token: pin })
if (delta === null) {
throw new BadRequestException('Невереый код')
}
await this.prismaService.user.update({
where: { id: user.id },
data: {
isTotpEnabled: true,
totpSecret: secret,
},
})
return true
}
async disable(user: User) {
await this.prismaService.user.update({
where: { id: user.id },
data: {
isTotpEnabled: false,
totpSecret: null,
},
})
return true
}
}

View File

@ -1,4 +1,4 @@
import { NewPasswordInput } from '@/src/module/auth/password-recovery/input/new-password.input'
import { NewPasswordInput } from '@/src/module/auth/password-recovery/inputs/new-password.input'
import { ValidationArguments, ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator'
@ValidatorConstraint({ name: 'IsPasswordMatching', async: false })

View File

@ -1,11 +1,11 @@
import { SessionInfo } from '@/src/shared/types/session-metadata.types'
import { InternalServerErrorException } from '@nestjs/common'
import { ConfigService } from '@nestjs/config';
import { ConfigService } from '@nestjs/config'
import { User } from '@prisma/generated'
import { Request } from 'express'
export function saveSession(req: Request, user: User, metadata: SessionInfo) {
return new Promise<User>((resolve, reject) => {
return new Promise((resolve, reject) => {
req.session.createdAt = new Date()
req.session.userId = user.id
req.session.metadata = metadata
@ -15,7 +15,7 @@ export function saveSession(req: Request, user: User, metadata: SessionInfo) {
reject(new InternalServerErrorException('Не удалось сохранить сессию'))
}
resolve(user)
resolve({ user })
})
})
}

View File

@ -1426,6 +1426,11 @@
dependencies:
tslib "2.8.1"
"@noble/hashes@1.7.1":
version "1.7.1"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.1.tgz#5738f6d765710921e7a751e00c20ae091ed8db0f"
integrity sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==
"@noble/hashes@^1.1.5":
version "1.8.0"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a"
@ -2140,6 +2145,13 @@
resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.10.tgz#52f8dbd6113517aef901db20b4f3fca543b88c1f"
integrity sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==
"@types/qrcode@^1.5.5":
version "1.5.5"
resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.5.5.tgz#993ff7c6b584277eee7aac0a20861eab682f9dac"
integrity sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==
dependencies:
"@types/node" "*"
"@types/qs@*":
version "6.14.0"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.14.0.tgz#d8b60cecf62f2db0fb68e5e006077b9178b85de5"
@ -3068,7 +3080,7 @@ camel-case@^3.0.0:
no-case "^2.2.0"
upper-case "^1.1.1"
camelcase@^5.3.1:
camelcase@^5.0.0, camelcase@^5.3.1:
version "5.3.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
@ -3225,6 +3237,15 @@ cli-width@^4.1.0:
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-4.1.0.tgz#42daac41d3c254ef38ad8ac037672130173691c5"
integrity sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==
cliui@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
dependencies:
string-width "^4.2.0"
strip-ansi "^6.0.0"
wrap-ansi "^6.2.0"
cliui@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
@ -3525,6 +3546,11 @@ debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3
dependencies:
ms "^2.1.3"
decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
decompress-response@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc"
@ -3632,6 +3658,11 @@ diff@^4.0.1:
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
dijkstrajs@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz#4c8dbdea1f0f6478bff94d9c49c784d623e4fc23"
integrity sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==
display-notification@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/display-notification/-/display-notification-2.0.0.tgz#49fad2e03289b4f668c296e1855c2cf8ba893d49"
@ -4495,7 +4526,7 @@ geoip-lite@^1.4.10:
rimraf "2.5.2 - 2.7.1"
yauzl "2.9.2 - 2.10.0"
get-caller-file@^2.0.5:
get-caller-file@^2.0.1, get-caller-file@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
@ -4728,6 +4759,11 @@ he@1.2.0, he@^1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hi-base32@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/hi-base32/-/hi-base32-0.5.1.tgz#1279f2ddae2673219ea5870c2121d2a33132857e"
integrity sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA==
html-escaper@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
@ -6610,6 +6646,13 @@ os-tmpdir@~1.0.2:
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==
otpauth@^9.4.0:
version "9.4.0"
resolved "https://registry.yarnpkg.com/otpauth/-/otpauth-9.4.0.tgz#ed52538848cc52138e4dcd75cef45412b6175ea1"
integrity sha512-fHIfzIG5RqCkK9cmV8WU+dPQr9/ebR5QOwGZn2JAr1RQF+lmAuLL2YdtdqvmBjNmgJlYk3KZ4a0XokaEhg1Jsw==
dependencies:
"@noble/hashes" "1.7.1"
p-cancelable@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-3.0.0.tgz#63826694b54d61ca1c20ebcb6d3ecf5e14cd8050"
@ -6841,6 +6884,11 @@ pluralize@8.0.0:
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1"
integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==
pngjs@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@ -7036,6 +7084,15 @@ pure-rand@^6.0.0:
resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2"
integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==
qrcode@^1.5.4:
version "1.5.4"
resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.4.tgz#5cb81d86eb57c675febb08cf007fff963405da88"
integrity sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==
dependencies:
dijkstrajs "^1.0.1"
pngjs "^5.0.0"
yargs "^15.3.1"
qs@6.13.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906"
@ -7189,6 +7246,11 @@ require-from-string@^2.0.2:
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
require-main-filename@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
resolve-alpn@^1.2.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9"
@ -7434,6 +7496,11 @@ serve-static@^2.2.0:
parseurl "^1.3.3"
send "^1.2.0"
set-blocking@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
setprototypeof@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
@ -8264,6 +8331,11 @@ whatwg-url@^5.0.0:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
which-module@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409"
integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==
which@^1.2.9:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
@ -8370,6 +8442,11 @@ xtend@^4.0.2:
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
y18n@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
y18n@^5.0.5:
version "5.0.8"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
@ -8385,6 +8462,31 @@ yargs-parser@21.1.1, yargs-parser@^21.1.1:
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
yargs-parser@^18.1.2:
version "18.1.3"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
dependencies:
camelcase "^5.0.0"
decamelize "^1.2.0"
yargs@^15.3.1:
version "15.4.1"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
dependencies:
cliui "^6.0.0"
decamelize "^1.2.0"
find-up "^4.1.0"
get-caller-file "^2.0.1"
require-directory "^2.1.1"
require-main-filename "^2.0.0"
set-blocking "^2.0.0"
string-width "^4.2.0"
which-module "^2.0.0"
y18n "^4.0.0"
yargs-parser "^18.1.2"
yargs@^17.3.1, yargs@^17.7.2:
version "17.7.2"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"