The endpoints for creation, deletion, as well as refs advertisement have been implemented.master
@@ -105,3 +105,6 @@ dist | |||
.tern-port | |||
.npmrc | |||
.database/ | |||
.idea/ | |||
repos/ |
@@ -48,6 +48,7 @@ | |||
"pridepack" | |||
], | |||
"devDependencies": { | |||
"@types/bcrypt": "^5.0.0", | |||
"@types/node": "^17.0.25", | |||
"eslint": "^8.13.0", | |||
"eslint-config-lxsmnsyc": "^0.4.0", | |||
@@ -57,9 +58,12 @@ | |||
"vitest": "^0.9.4" | |||
}, | |||
"dependencies": { | |||
"@prisma/client": "^3.14.0", | |||
"@theoryofnekomata/uuid-buffer": "^0.1.0", | |||
"bcrypt": "^5.0.1", | |||
"fastify": "^3.27.0", | |||
"fastify-plugin": "^3.0.1" | |||
"fastify-plugin": "^3.0.1", | |||
"prisma": "^3.14.0" | |||
}, | |||
"scripts": { | |||
"prepublishOnly": "pridepack clean && pridepack build", | |||
@@ -0,0 +1,83 @@ | |||
// This is your Prisma schema file, | |||
// learn more about it in the docs: https://pris.ly/d/prisma-schema | |||
generator client { | |||
provider = "prisma-client-js" | |||
} | |||
datasource db { | |||
provider = "sqlite" | |||
url = env("DATABASE_URL") | |||
} | |||
model User { | |||
id Bytes @id | |||
username String @unique | |||
password String | |||
createdAt DateTime @default(now()) | |||
properties Property[] | |||
userOrgs UserOrg[] | |||
sessions Session[] | |||
createdOrgs Org[] | |||
} | |||
model Property { | |||
id Bytes @id | |||
userId Bytes | |||
user User @relation(fields: [userId], references: [id]) | |||
name String | |||
value String | |||
} | |||
model Org { | |||
id Bytes @id | |||
name String @unique | |||
description String | |||
createdAt DateTime @default(now()) | |||
creatorUserId Bytes | |||
creatorUser User @relation(fields: [creatorUserId], references: [id]) | |||
orgUsers UserOrg[] | |||
} | |||
model UserOrg { | |||
id Int @id @default(autoincrement()) | |||
userId Bytes | |||
user User @relation(fields: [userId], references: [id]) | |||
orgId Bytes | |||
org Org @relation(fields: [orgId], references: [id]) | |||
} | |||
model Repo { | |||
id Bytes @id | |||
name String | |||
visibility String | |||
ownerId Bytes | |||
ownerType String | |||
createdAt DateTime @default(now()) | |||
} | |||
model Log { | |||
id Int @id @default(autoincrement()) | |||
subjectUserId Bytes | |||
subjectUsername String | |||
action String | |||
createdAt DateTime @default(now()) | |||
parameters LogParameter[] | |||
} | |||
model LogParameter { | |||
id Int @id @default(autoincrement()) | |||
logId Int | |||
log Log @relation(fields: [logId], references: [id]) | |||
key String | |||
value String | |||
} | |||
model Session { | |||
id Bytes @id | |||
createdAt DateTime | |||
validUntil DateTime | |||
userId Bytes | |||
user User @relation(fields: [userId], references: [id]) | |||
} |
@@ -0,0 +1,14 @@ | |||
import { Uuid } from '@theoryofnekomata/uuid-buffer'; | |||
import { UserService, UserServiceImpl } from '../src/modules/user' | |||
const main = async () => { | |||
const userService: UserService = new UserServiceImpl() | |||
const newUser = await userService.create({ | |||
username: 'username', | |||
password: 'password', | |||
}) | |||
console.log(Uuid.from(newUser.id).toString()); | |||
} | |||
void main() |
@@ -1,7 +1,9 @@ | |||
import { createServer } from './server'; | |||
import { addRoutes } from './routes'; | |||
const server = createServer(); | |||
const server = createServer({ | |||
logger: true, | |||
}); | |||
addRoutes(server); | |||
server.listen(process.env.PORT ?? 8080, '0.0.0.0', (err) => { | |||
@@ -0,0 +1,63 @@ | |||
import { RouteHandlerMethod } from 'fastify' | |||
import { Uuid } from '@theoryofnekomata/uuid-buffer' | |||
import { Controller } from 'src/packages/fastify-utils-theoryofnekomata' | |||
import { UserService, UserServiceImpl } from 'src/modules/user' | |||
import { LoginUserFormData } from 'src/modules/auth/models' | |||
import { SessionService, SessionServiceImpl } from 'src/modules/auth/Session.service' | |||
import { | |||
LoggedOutData, | |||
LoggedInData, | |||
UnableToLogInError, | |||
UnableToLogOutError, | |||
} from 'src/modules/auth/responses' | |||
export interface AuthController extends Controller<'logIn' | 'logOut'> {} | |||
export class AuthControllerImpl implements AuthController { | |||
private readonly sessionService: SessionService | |||
private readonly userService: UserService | |||
constructor() { | |||
this.sessionService = new SessionServiceImpl() | |||
this.userService = new UserServiceImpl() | |||
} | |||
readonly logIn: RouteHandlerMethod = async (request, reply) => { | |||
const { username, password } = request.body as LoginUserFormData | |||
try { | |||
const existingUser = await this.userService.getFromCredentials({ | |||
username, | |||
password, | |||
}) | |||
const newSession = await this.sessionService.create({ | |||
userId: existingUser.id, | |||
}) | |||
reply.sendData(new LoggedInData(newSession)) | |||
} catch (causeRaw) { | |||
const cause = causeRaw as Error | |||
throw new UnableToLogInError( | |||
'Authorization could not be performed on the credentials provided. Either try again later or ensure registration using the previous credentials.', | |||
{ cause }, | |||
) | |||
} | |||
} | |||
readonly logOut: RouteHandlerMethod = async (request, reply) => { | |||
const sessionId = request.session?.id; | |||
if (sessionId) { | |||
try { | |||
await this.sessionService.expire(Uuid.from(sessionId)) | |||
reply.sendData(new LoggedOutData()) | |||
return | |||
} catch { | |||
// noop | |||
} | |||
} | |||
throw new UnableToLogOutError( | |||
'De-authorization could not be performed. Ensure session data is present and/or clear local data to carry out the operation.') | |||
} | |||
} |
@@ -0,0 +1,29 @@ | |||
import { hash, genSalt, compare } from 'bcrypt' | |||
import { AppError } from 'src/packages/fastify-compliant-http-errors' | |||
export class PasswordNotEqualAssertError extends AppError { | |||
} | |||
export interface PasswordService { | |||
hash(password: string): Promise<string>; | |||
assertEqual(password: string, hashedPassword: string): Promise<void>; | |||
} | |||
export class PasswordServiceImpl implements PasswordService { | |||
constructor(private readonly saltRounds = 12) { | |||
} | |||
async hash(password: string): Promise<string> { | |||
const salt = await genSalt(this.saltRounds) | |||
return hash(password, salt) | |||
} | |||
async assertEqual(password: string, hashedPassword: string): Promise<void> { | |||
const result = await compare(password, hashedPassword) | |||
if (result) { | |||
return | |||
} | |||
throw new PasswordNotEqualAssertError() | |||
} | |||
} |
@@ -0,0 +1,120 @@ | |||
import { PrismaClient } from '@prisma/client' | |||
import { CreateSessionData, Session } from 'src/modules/auth/models' | |||
import { AppError } from 'src/packages/fastify-compliant-http-errors' | |||
import {Uuid} from '@theoryofnekomata/uuid-buffer'; | |||
import {notFoundFactory} from 'src/packages/prisma-error-utils'; | |||
import {User} from '../user'; | |||
type SessionWithUsername = Session & { user: Pick<User, 'username'> }; | |||
export interface SessionService { | |||
create(data: CreateSessionData): Promise<SessionWithUsername> | |||
expire(sessionId: Session['id']): Promise<Session> | |||
isValid(sessionId: Session['id']): Promise<boolean> | |||
get(id: Session['id']): Promise<SessionWithUsername | null> | |||
} | |||
export class SessionNotFoundError extends AppError {} | |||
export class SessionServiceImpl implements SessionService { | |||
private readonly prismaClient: PrismaClient | |||
constructor() { | |||
this.prismaClient = new PrismaClient() | |||
} | |||
private static serialize<T extends Session>(raw: T): T { | |||
return { | |||
...raw, | |||
id: Uuid.from(raw.id), | |||
userId: Uuid.from(raw.userId) | |||
}; | |||
} | |||
async create(data: CreateSessionData): Promise<SessionWithUsername> { | |||
const createdAt = new Date() | |||
const validUntil = new Date(createdAt) | |||
validUntil.setDate(validUntil.getDate() + 1) | |||
const raw = await this.prismaClient.session.create({ | |||
data: { | |||
...data, | |||
id: Uuid.v4(), | |||
createdAt, | |||
validUntil, | |||
}, | |||
include: { | |||
user: { | |||
select: { | |||
id: false, | |||
username: true, | |||
password: false, | |||
}, | |||
}, | |||
}, | |||
}) | |||
return SessionServiceImpl.serialize(raw); | |||
} | |||
async expire(sessionId: Session['id']): Promise<Session> { | |||
const existingSession = await this.prismaClient.session.findUnique({ | |||
where: { | |||
id: sessionId, | |||
}, | |||
rejectOnNotFound: notFoundFactory(SessionNotFoundError), | |||
}) | |||
const raw = await this.prismaClient.session.update({ | |||
where: { | |||
id: existingSession.id, | |||
}, | |||
data: { | |||
validUntil: new Date(), | |||
}, | |||
}) | |||
return SessionServiceImpl.serialize(raw); | |||
} | |||
async isValid(sessionId: Session['id']): Promise<boolean> { | |||
const data = await this.prismaClient.session.findUnique({ | |||
where: { | |||
id: sessionId, | |||
}, | |||
}) | |||
if (!data) { | |||
return false | |||
} | |||
const now = new Date() | |||
return ( | |||
data.createdAt <= now | |||
&& now < data.validUntil | |||
) | |||
} | |||
async get(id: Session['id']): Promise<SessionWithUsername | null> { | |||
const raw = await this.prismaClient.session.findUnique({ | |||
where: { | |||
id, | |||
}, | |||
include: { | |||
user: { | |||
select: { | |||
id: false, | |||
username: true, | |||
password: false, | |||
} | |||
} | |||
} | |||
}) | |||
if (raw) { | |||
return SessionServiceImpl.serialize(raw); | |||
} | |||
return null; | |||
} | |||
} |
@@ -0,0 +1,5 @@ | |||
export * from './Auth.controller' | |||
export * from './models' | |||
export * from './Password.service' | |||
export * from './Session.service' | |||
export * from './responses' |
@@ -0,0 +1,14 @@ | |||
export interface Session { | |||
id: Buffer | |||
createdAt: Date | |||
validUntil: Date | |||
userId: Buffer | |||
} | |||
export interface CreateSessionData extends Omit<Session, 'id' | 'createdAt' | 'validUntil'> { | |||
} | |||
export interface LoginUserFormData { | |||
username: string; | |||
password: string; | |||
} |
@@ -0,0 +1,16 @@ | |||
import { HttpResponse } from 'src/packages/fastify-send-data' | |||
import { Session } from 'src/modules/auth/models' | |||
import { constants } from 'http2' | |||
import { HttpError } from 'src/packages/fastify-compliant-http-errors' | |||
export class LoggedInData extends HttpResponse<Session>(constants.HTTP_STATUS_OK, 'Logged In') { | |||
} | |||
export class LoggedOutData extends HttpResponse(constants.HTTP_STATUS_NO_CONTENT, 'Logged Out') { | |||
} | |||
export class UnableToLogInError extends HttpError(constants.HTTP_STATUS_UNAUTHORIZED, 'Unable to Log In') { | |||
} | |||
export class UnableToLogOutError extends HttpError(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, 'Unable to Log Out') { | |||
} |
@@ -0,0 +1,18 @@ | |||
import {ServerInstance} from '../../packages/fastify-utils-theoryofnekomata'; | |||
import {AuthController, AuthControllerImpl} from './Auth.controller'; | |||
export const addRoutes = (server: ServerInstance) => { | |||
const authController: AuthController = new AuthControllerImpl() | |||
server.route({ | |||
method: 'POST', | |||
url: '/api/auth/log-in', | |||
handler: authController.logIn, | |||
}); | |||
server.route({ | |||
method: 'POST', | |||
url: '/api/auth/log-out', | |||
handler: authController.logOut, | |||
}); | |||
} |
@@ -1,34 +0,0 @@ | |||
import {GitService, GitServiceImpl} from './Git.service'; | |||
import {RouteHandlerMethod} from 'fastify'; | |||
import {constants} from 'http2'; | |||
import * as codeCore from '@modal/code-core'; | |||
import {HttpResponse} from '../../packages/fastify-send-data'; | |||
import Controller from '../../utils/types'; | |||
import {Uuid} from '@theoryofnekomata/uuid-buffer'; | |||
export interface GitController extends Controller< | |||
'createRepo' | |||
| 'deleteRepo' | |||
> {} | |||
class RepoCreatedResponse extends HttpResponse<codeCore.git.Repo>(constants.HTTP_STATUS_CREATED, 'Repo Created') {} | |||
class RepoDeletedResponse extends HttpResponse(constants.HTTP_STATUS_NO_CONTENT, 'Repo Deleted') {} | |||
export class GitControllerImpl implements GitController { | |||
private readonly gitService: GitService; | |||
constructor() { | |||
this.gitService = new GitServiceImpl() | |||
} | |||
readonly createRepo: RouteHandlerMethod = async (request, reply) => { | |||
const repo = await this.gitService.createRepo(request.body as codeCore.git.CreateRepoData, request.user); | |||
reply.sendData(new RepoCreatedResponse(repo)); | |||
} | |||
readonly deleteRepo: RouteHandlerMethod = async (request, reply) => { | |||
const params = request.params as { repoId: string } | |||
await this.gitService.deleteRepo(Uuid.from(params.repoId), request.user) | |||
reply.sendData(new RepoDeletedResponse()); | |||
} | |||
} |
@@ -1,48 +0,0 @@ | |||
import {constants} from 'http2'; | |||
import * as codeCore from '@modal/code-core'; | |||
import {HttpError} from '../../packages/fastify-compliant-http-errors'; | |||
export interface GitService { | |||
createRepo(options: codeCore.git.CreateRepoData, user?: codeCore.common.User): Promise<codeCore.git.Repo>; | |||
deleteRepo(repoId: codeCore.git.Repo['id'], user?: codeCore.common.User): Promise<void> | |||
} | |||
export class UnauthorizedToCreateRepoError extends HttpError(constants.HTTP_STATUS_UNAUTHORIZED, 'Unauthorized to Create Repo') {} | |||
export class UnauthorizedToDeleteRepoError extends HttpError(constants.HTTP_STATUS_UNAUTHORIZED, 'Unauthorized to Delete Repo') {} | |||
export class UnableToCreateRepoError extends HttpError(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, 'Unable to Create Repo') {} | |||
export class UnableToDeleteRepoError extends HttpError(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, 'Unable to Delete Repo') {} | |||
export class GitServiceImpl implements GitService { | |||
private readonly coreGitService: codeCore.git.GitService; | |||
constructor() { | |||
this.coreGitService = new codeCore.git.GitServiceImpl(); | |||
} | |||
async createRepo(options: codeCore.git.CreateRepoData, user?: codeCore.common.User): Promise<codeCore.git.Repo> { | |||
if (user) { | |||
let newRepo: codeCore.git.Repo; | |||
try { | |||
newRepo = await this.coreGitService.create(options, user); | |||
} catch (causeRaw) { | |||
const cause = causeRaw as Error; | |||
throw new UnableToCreateRepoError('Something went wrong while creating the repo.', { cause, }); | |||
} | |||
return newRepo; | |||
} | |||
throw new UnauthorizedToCreateRepoError('Could not create repo with insufficient authorization.'); | |||
} | |||
async deleteRepo(repoId: codeCore.git.Repo['id'], user?: codeCore.common.User): Promise<void> { | |||
if (user) { | |||
try { | |||
await this.coreGitService.delete(repoId, user); | |||
} catch (causeRaw) { | |||
const cause = causeRaw as Error; | |||
throw new UnableToDeleteRepoError('Something went wrong while deleting the repo.', { cause, }); | |||
} | |||
return; | |||
} | |||
throw new UnauthorizedToDeleteRepoError('Could not delete repo with insufficient authorization.'); | |||
} | |||
} |
@@ -1,3 +0,0 @@ | |||
export * from './Git.service' | |||
export * from './Git.controller' | |||
export * from './models' |
@@ -1,3 +0,0 @@ | |||
import * as codeCore from '@modal/code-core' | |||
export type CreateRepoOptions = codeCore.git.CreateOptions |
@@ -0,0 +1,42 @@ | |||
import { Prisma, PrismaClient } from '@prisma/client'; | |||
import {Uuid} from '@theoryofnekomata/uuid-buffer'; | |||
import {User} from '../user'; | |||
import {Log} from './models'; | |||
export interface LogService { | |||
create(subject: User, action: string, parameters?: Record<string, unknown>): Promise<Log> | |||
} | |||
export class LogServiceImpl implements LogService { | |||
private readonly prismaClient: PrismaClient | |||
constructor() { | |||
this.prismaClient = new PrismaClient() | |||
} | |||
async create(subject: User, action: string, parameters?: Record<string, unknown>): Promise<Log> { | |||
const createData: Prisma.LogCreateInput = { | |||
subjectUsername: subject.username, | |||
subjectUserId: subject.id, | |||
action, | |||
} | |||
if (parameters) { | |||
createData['parameters'] = { | |||
create: Object.entries(parameters).map(([key, value]) => ({ | |||
key, | |||
value: `${value}`, | |||
})), | |||
} | |||
} | |||
const raw = await this.prismaClient.log.create({ | |||
data: createData, | |||
}) | |||
return { | |||
...raw, | |||
subjectUserId: Uuid.from(raw.subjectUserId), | |||
} | |||
} | |||
} |
@@ -0,0 +1,2 @@ | |||
export * from './Log.service' | |||
export * from './models' |
@@ -0,0 +1,7 @@ | |||
export type Log = { | |||
id: number, | |||
subjectUserId: Buffer, | |||
subjectUsername: string, | |||
action: string, | |||
createdAt: Date, | |||
} |
@@ -0,0 +1,36 @@ | |||
import {PrismaClient} from '@prisma/client'; | |||
import {Org} from './models'; | |||
import {Uuid} from '@theoryofnekomata/uuid-buffer'; | |||
import {notFoundFactory} from '../../packages/prisma-error-utils'; | |||
import {OrgNotFoundError} from './responses'; | |||
export interface OrgService { | |||
getFromId(orgId: Org['id']): Promise<Org>; | |||
} | |||
export class OrgServiceImpl { | |||
private readonly prismaClient: PrismaClient; | |||
constructor() { | |||
this.prismaClient = new PrismaClient(); | |||
} | |||
private static serialize<T extends Org>(org: T): T { | |||
return { | |||
...org, | |||
id: Uuid.from(org.id), | |||
creatorUserId: Uuid.from(org.creatorUserId), | |||
}; | |||
} | |||
async getFromId(id: Org['id']): Promise<Org> { | |||
const org = await this.prismaClient.org.findUnique({ | |||
where: { | |||
id, | |||
}, | |||
rejectOnNotFound: notFoundFactory(OrgNotFoundError), | |||
}) | |||
return OrgServiceImpl.serialize(org); | |||
} | |||
} |
@@ -0,0 +1,3 @@ | |||
export * from './models'; | |||
export * from './Org.service'; | |||
export * from './responses'; |
@@ -0,0 +1,6 @@ | |||
export type Org = { | |||
id: Buffer, | |||
name: string, | |||
description: string, | |||
creatorUserId: Buffer, | |||
} |
@@ -0,0 +1,4 @@ | |||
import { constants } from 'http2' | |||
import {HttpError} from 'src/packages/fastify-compliant-http-errors'; | |||
export class OrgNotFoundError extends HttpError(constants.HTTP_STATUS_NOT_FOUND, 'Org Not Found') {} |
@@ -0,0 +1,73 @@ | |||
import {Uuid} from '@theoryofnekomata/uuid-buffer'; | |||
import {OwnerType} from './models'; | |||
import {UnknownOwnerTypeError} from './responses'; | |||
import {UserNotFoundError, UserService, UserServiceImpl} from '../user'; | |||
import {OrgNotFoundError, OrgService, OrgServiceImpl} from '../org'; | |||
export interface OwnerService { | |||
getOwnerName(id: Uuid, type: string): Promise<string>; | |||
getOwnerTypeDirName(type: string): string; | |||
} | |||
export class OwnerServiceImpl implements OwnerService { | |||
private readonly userService: UserService; | |||
private readonly orgService: OrgService; | |||
private readonly OWNER_TYPE_DIR_NAME: Record<OwnerType, string>; | |||
constructor() { | |||
this.userService = new UserServiceImpl(); | |||
this.orgService = new OrgServiceImpl(); | |||
this.OWNER_TYPE_DIR_NAME = { | |||
[OwnerType.USER]: 'users', | |||
[OwnerType.ORG]: 'orgs', | |||
}; | |||
} | |||
private async getOwnerNameFromUser(ownerId: Uuid) { | |||
try { | |||
const user = await this.userService.getFromId(ownerId); | |||
return user.username; | |||
} catch (err) { | |||
if (!(err instanceof UserNotFoundError)) { | |||
throw err; | |||
} | |||
} | |||
return ownerId.toString(); | |||
} | |||
private async getOwnerNameFromOrg(ownerId: Uuid) { | |||
try { | |||
const org = await this.orgService.getFromId(ownerId); | |||
return org.name; | |||
} catch (err) { | |||
if (!(err instanceof OrgNotFoundError)) { | |||
throw err; | |||
} | |||
} | |||
return ownerId.toString(); | |||
} | |||
async getOwnerName(ownerId: Uuid, ownerType: string) { | |||
if (ownerType === OwnerType.USER) { | |||
return this.getOwnerNameFromUser(ownerId); | |||
} | |||
if (ownerType === OwnerType.ORG) { | |||
return this.getOwnerNameFromOrg(ownerId); | |||
} | |||
throw new UnknownOwnerTypeError(`Only valid options are: ${Object.values(OwnerType).join(', ')}`); | |||
} | |||
getOwnerTypeDirName(type: string): string { | |||
const { [type as OwnerType]: dirName } = this.OWNER_TYPE_DIR_NAME; | |||
if (!dirName) { | |||
throw new UnknownOwnerTypeError(`Only valid options are: ${Object.values(OwnerType).join(', ')}`); | |||
} | |||
return dirName; | |||
} | |||
} |
@@ -0,0 +1,3 @@ | |||
export * from './models' | |||
export * from './responses' | |||
export * from './Owner.service' |
@@ -0,0 +1,4 @@ | |||
export enum OwnerType { | |||
USER = 'USER', | |||
ORG = 'ORG', | |||
} |
@@ -0,0 +1,4 @@ | |||
import {HttpError} from '../../packages/fastify-compliant-http-errors'; | |||
import {constants} from 'http2'; | |||
export class UnknownOwnerTypeError extends HttpError(constants.HTTP_STATUS_BAD_REQUEST, 'Unknown Owner Type') {} |
@@ -0,0 +1,53 @@ | |||
import {User} from '../user'; | |||
import {Repo, RepoAction} from '../repo'; | |||
import {LogService, LogServiceImpl} from '../log'; | |||
export interface RepoLogService { | |||
logCreateRepo(user: User, repoMetadata: Repo, ownerName: string): Promise<void>; | |||
logDeleteRepo(user: User, repo: Repo): Promise<void>; | |||
} | |||
export class RepoLogServiceImpl implements RepoLogService { | |||
private readonly logService: LogService; | |||
constructor() { | |||
this.logService = new LogServiceImpl(); | |||
} | |||
private async doLogCreateRepo(user: User, repoMetadata: Repo, ownerName: string): Promise<void> { | |||
try { | |||
await this.logService.create( | |||
user, | |||
RepoAction.REPO_CREATED, | |||
{ | |||
repoId: repoMetadata.id.toString(), | |||
repoName: repoMetadata.name, | |||
ownerId: repoMetadata.ownerId.toString(), | |||
ownerName, | |||
ownerType: repoMetadata.ownerType, | |||
}, | |||
); | |||
} catch {} | |||
} | |||
async logCreateRepo(user: User, repoMetadata: Repo, ownerName: string): Promise<void> { | |||
await this.doLogCreateRepo(user, repoMetadata, ownerName); | |||
} | |||
private async doLogDeleteRepo(user: User, repo: Repo): Promise<void> { | |||
try { | |||
await this.logService.create( | |||
user, | |||
RepoAction.REPO_REMOVED, | |||
{ | |||
repoId: repo.id.toString(), | |||
repoName: repo.name, | |||
}, | |||
); | |||
} catch {} | |||
} | |||
async logDeleteRepo(user: User, repo: Repo): Promise<void> { | |||
await this.doLogDeleteRepo(user, repo); | |||
} | |||
} |
@@ -0,0 +1 @@ | |||
export * from './RepoLog.service'; |
@@ -0,0 +1,73 @@ | |||
import {RepoService, RepoServiceImpl} from './Repo.service'; | |||
import {RouteHandlerMethod} from 'fastify'; | |||
import {constants} from 'http2'; | |||
import {HttpResponse} from '../../packages/fastify-send-data'; | |||
import { Controller } from 'src/packages/fastify-utils-theoryofnekomata' | |||
import {Uuid} from '@theoryofnekomata/uuid-buffer'; | |||
import {CreateRepoData, Repo} from './models'; | |||
export interface RepoController extends Controller< | |||
'createRepo' | |||
| 'deleteRepo' | |||
| 'getRepoRefs' | |||
| 'receivePack' | |||
> {} | |||
class RepoCreatedResponse extends HttpResponse<Repo>(constants.HTTP_STATUS_CREATED, 'Repo Created') {} | |||
class RepoDeletedResponse extends HttpResponse(constants.HTTP_STATUS_NO_CONTENT, 'Repo Deleted') {} | |||
export class RepoControllerImpl implements RepoController { | |||
private readonly repoService: RepoService; | |||
private readonly noCacheHeaders = { | |||
'Expires': 'Fri, 01 Jan 1980 00:00:00 GMT', | |||
'Pragma': 'no-cache', | |||
'Cache-Control': 'no-cache, max-age=0, must-revalidate', | |||
}; | |||
constructor() { | |||
this.repoService = new RepoServiceImpl(); | |||
} | |||
readonly createRepo: RouteHandlerMethod = async (request, reply) => { | |||
const repo = await this.repoService.createRepo(request.body as CreateRepoData, request.session?.user); | |||
reply.sendData(new RepoCreatedResponse(repo)); | |||
} | |||
readonly deleteRepo: RouteHandlerMethod = async (request, reply) => { | |||
const params = request.params as { repoId: string } | |||
await this.repoService.deleteRepo(Uuid.from(params.repoId), request.session?.user) | |||
reply.sendData(new RepoDeletedResponse()); | |||
} | |||
readonly getRepoRefs: RouteHandlerMethod = async (request, reply) => { | |||
const { service } = request.query as { service: string }; | |||
const params = request.params as { repoName: string, ownerName: string, ownerType: string } | |||
const outputStream = await this.repoService.getRefsFromService( | |||
params.ownerType, | |||
params.ownerName, | |||
params.repoName, | |||
service, | |||
request.session?.user, | |||
); | |||
reply | |||
.headers({ | |||
...this.noCacheHeaders, | |||
'Content-Type': `application/x-${service}-advertisement`, | |||
}) | |||
.send(outputStream); | |||
} | |||
readonly receivePack: RouteHandlerMethod = async (request, reply) => { | |||
console.log('content-type', request.headers['content-type']); | |||
console.log('body', typeof request.body, request.body, (request.body as Buffer).toString('utf-8')); | |||
reply | |||
.headers({ | |||
...this.noCacheHeaders, | |||
// Mark this as service response | |||
'Content-Type': 'application/x-git-receive-pack-result', | |||
}) | |||
.status(500) | |||
.send(''); | |||
} | |||
} |
@@ -0,0 +1,299 @@ | |||
import {PrismaClient} from '@prisma/client'; | |||
import {PassThrough, Stream} from 'stream'; | |||
import {Uuid} from '@theoryofnekomata/uuid-buffer'; | |||
import * as codeCore from '@modal/code-core'; | |||
import {notFoundFactory} from '../../packages/prisma-error-utils'; | |||
import {User} from '../user'; | |||
import {CreateRepoData, Repo, RepoAvailableService} from './models'; | |||
import { | |||
UnauthorizedToCreateRepoError, | |||
UnauthorizedToDeleteRepoError, | |||
UnauthorizedToInvokeRepoServiceError, | |||
UnableToCreateRepoError, | |||
UnableToDeleteRepoError, | |||
UnableToInvokeRepoServiceError, | |||
UnknownServiceError, | |||
RepoNotFoundError, | |||
} from './responses'; | |||
import {ChildProcess} from 'child_process'; | |||
import path from 'path'; | |||
import {OwnerService, OwnerServiceImpl, OwnerType} from '../owner'; | |||
import {RepoLogService, RepoLogServiceImpl} from '../repo-log'; | |||
type GitServiceMethod = (cwd: string) => Promise<ChildProcess>; | |||
export interface RepoService { | |||
createRepo(data: CreateRepoData, user?: User): Promise<Repo>; | |||
getSingleRepoById(repoId: Repo['id']): Promise<Repo>; | |||
getSingleRepoByOwnerAndName(ownerType: Repo['ownerType'], ownerId: Repo['ownerId'], repoName: Repo['name']): Promise<Repo>; | |||
getOwnedRepos(ownerType: Repo['ownerType'], ownerId: Repo['ownerId']): Promise<Repo[]>; | |||
deleteRepo(repoId: Repo['id'], user?: User): Promise<void>; | |||
getRefsFromService( | |||
ownerType: Repo['ownerType'], | |||
ownerName: Repo['name'], | |||
repoName: Repo['name'], | |||
service?: string, | |||
user?: User, | |||
): Promise<Stream>; | |||
} | |||
export class RepoServiceImpl implements RepoService { | |||
private readonly gitService: codeCore.git.GitService; | |||
private readonly repoLogService: RepoLogService; | |||
private readonly prismaClient: PrismaClient; | |||
private readonly ownerService: OwnerService; | |||
constructor() { | |||
this.gitService = new codeCore.git.GitServiceImpl(); | |||
this.repoLogService = new RepoLogServiceImpl(); | |||
this.prismaClient = new PrismaClient(); | |||
this.ownerService = new OwnerServiceImpl(); | |||
} | |||
private static serialize<T extends Repo>(repoMetadataRaw: T): T { | |||
return { | |||
...repoMetadataRaw, | |||
id: Uuid.from(repoMetadataRaw.id), | |||
ownerId: Uuid.from(repoMetadataRaw.ownerId), | |||
}; | |||
} | |||
private static getRepoBasePath(ownerType: string, ownerName: string, repoName: string) { | |||
return path.join('repos', ownerType, ownerName, repoName.endsWith('.git') ? repoName : `${repoName}.git`); | |||
} | |||
private static authenticate(_user?: User): User { | |||
return { | |||
id: Uuid.v4(), | |||
username: 'foo', | |||
} as User; | |||
} | |||
private async doCreateRepoFilesProcess(repoName: string, ownerName: string, ownerType: OwnerType): Promise<ChildProcess> { | |||
try { | |||
const ownerTypeDirName = this.ownerService.getOwnerTypeDirName(ownerType); | |||
const repoBasePath = RepoServiceImpl.getRepoBasePath(repoName, ownerName, ownerTypeDirName); | |||
return this.gitService.create(repoBasePath); | |||
} catch (causeRaw) { | |||
const cause = causeRaw as Error; | |||
throw new UnableToCreateRepoError('Something went wrong while creating the repo.', { cause, }); | |||
} | |||
} | |||
private async doCreateRepoFiles(repoName: string, ownerName: string, ownerType: OwnerType): Promise<void> { | |||
const childProcess = await this.doCreateRepoFilesProcess(repoName, ownerName, ownerType); | |||
return new Promise((resolve, reject) => { | |||
childProcess.on('error', (causeRaw) => { | |||
const cause = causeRaw as Error; | |||
reject(new UnableToCreateRepoError('Something went wrong while creating the repo.', { cause, })); | |||
}); | |||
childProcess.on('close', (code) => { | |||
if (code !== 0) { | |||
reject(new UnableToCreateRepoError('Something went wrong while creating the repo.', { | |||
cause: new Error(`Git process returned ${code}`), | |||
})); | |||
return; | |||
} | |||
resolve(); | |||
}); | |||
}) | |||
} | |||
private async doCreateRepoMetadata(repoId: Repo['id'], data: CreateRepoData): Promise<Repo> { | |||
let repoMetadataRaw: Repo; | |||
try { | |||
repoMetadataRaw = await this.prismaClient.repo.create({ | |||
data: { | |||
id: repoId, | |||
name: data.name, | |||
visibility: data.visibility, | |||
ownerId: data.ownerId, | |||
ownerType: data.ownerType, | |||
}, | |||
}); | |||
} catch (causeRaw) { | |||
const cause = causeRaw as Error; | |||
throw new UnableToCreateRepoError('Something went wrong while creating the repo.', { cause, }); | |||
} | |||
return RepoServiceImpl.serialize(repoMetadataRaw); | |||
} | |||
async createRepo(data: CreateRepoData, userRaw?: User): Promise<Repo> { | |||
// TODO put this in ability service | |||
const user = RepoServiceImpl.authenticate(userRaw); | |||
if (!user) { | |||
throw new UnauthorizedToCreateRepoError('Could not create repo with insufficient authorization.'); | |||
} | |||
const repoId = Uuid.v4(); | |||
const ownerId = Uuid.from(data.ownerId); | |||
const ownerName = await this.ownerService.getOwnerName(ownerId, data.ownerType); | |||
await this.doCreateRepoFiles(data.name, ownerName, data.ownerType as OwnerType); | |||
const repoMetadata = await this.doCreateRepoMetadata(repoId, { | |||
...data, | |||
ownerId, | |||
}); | |||
await this.repoLogService.logCreateRepo(user, repoMetadata, ownerName); | |||
return repoMetadata; | |||
} | |||
private async doGetSingleRepo(repoId: Repo['id']): Promise<Repo> { | |||
const repo = await this.prismaClient.repo.findUnique({ | |||
where: { | |||
id: repoId, | |||
}, | |||
rejectOnNotFound: notFoundFactory(RepoNotFoundError), | |||
}) | |||
return RepoServiceImpl.serialize(repo); | |||
} | |||
async getSingleRepoById(repoId: Repo['id']): Promise<Repo> { | |||
return this.doGetSingleRepo(repoId); | |||
} | |||
private async doGetSingleRepoByOwnerAndName( | |||
ownerType: Repo['ownerType'], | |||
ownerId: Repo['ownerId'], | |||
repoName: Repo['name'] | |||
): Promise<Repo> { | |||
const repo = await this.prismaClient.repo.findFirst({ | |||
where: { | |||
ownerId: Uuid.from(ownerId), | |||
ownerType, | |||
name: repoName, | |||
}, | |||
rejectOnNotFound: notFoundFactory(RepoNotFoundError), | |||
}) | |||
return RepoServiceImpl.serialize(repo); | |||
} | |||
async getSingleRepoByOwnerAndName( | |||
ownerType: Repo['ownerType'], | |||
ownerId: Repo['ownerId'], | |||
repoName: Repo['name'] | |||
): Promise<Repo> { | |||
return this.doGetSingleRepoByOwnerAndName(ownerType, ownerId, repoName); | |||
} | |||
private async doDeleteRepo(repoName: string, ownerName: string, ownerType: OwnerType): Promise<void> { | |||
try { | |||
const ownerTypeDirName = this.ownerService.getOwnerTypeDirName(ownerType); | |||
const repoBasePath = RepoServiceImpl.getRepoBasePath(repoName, ownerName, ownerTypeDirName); | |||
await this.gitService.delete(repoBasePath); | |||
} catch (causeRaw) { | |||
const cause = causeRaw as Error; | |||
throw new UnableToDeleteRepoError('Something went wrong while deleting the repo.', { cause, }); | |||
} | |||
} | |||
async deleteRepo(repoId: Repo['id'], userRaw?: User): Promise<void> { | |||
const user = RepoServiceImpl.authenticate(userRaw); | |||
if (!user) { | |||
throw new UnauthorizedToDeleteRepoError('Could not delete repo with insufficient authorization.'); | |||
} | |||
const repo = await this.doGetSingleRepo(repoId); | |||
const ownerName = await this.ownerService.getOwnerName(repo.ownerId, repo.ownerType); | |||
await this.doDeleteRepo(repo.name, ownerName, repo.ownerType as OwnerType); | |||
await this.repoLogService.logDeleteRepo(user, repo); | |||
} | |||
private async doGetOwnedRepos(ownerType: Repo['ownerType'], ownerId: Repo['ownerId']) { | |||
const repos = await this.prismaClient.repo.findMany({ | |||
where: { | |||
ownerId, | |||
ownerType, | |||
}, | |||
}) | |||
return repos.map((r) => ({ | |||
...r, | |||
id: Uuid.from(r.id), | |||
ownerId: Uuid.from(r.ownerId), | |||
})); | |||
} | |||
async getOwnedRepos(ownerType: Repo['ownerType'], ownerId: Repo['ownerId']): Promise<Repo[]> { | |||
return this.doGetOwnedRepos(ownerType, ownerId); | |||
} | |||
private static gitPackSideband(s: string): string { | |||
return `${(4 + s.length).toString(16).padStart(4, '0')}${s}`; | |||
} | |||
async doGetRefsFromService( | |||
ownerType: Repo['ownerType'], | |||
ownerName: Repo['name'], | |||
repoName: Repo['name'], | |||
service?: string, | |||
) { | |||
if (!service) { | |||
throw new UnknownServiceError('Could not return refs for service.'); | |||
} | |||
const gitServiceMethods: Record<RepoAvailableService, GitServiceMethod> = { | |||
[RepoAvailableService.RECEIVE_PACK]: this.gitService.advertiseReceivePackRefs, | |||
[RepoAvailableService.UPLOAD_PACK]: this.gitService.advertiseUploadPackRefs, | |||
} | |||
const { [service as RepoAvailableService]: childProcessMethod } = gitServiceMethods; | |||
if (!childProcessMethod) { | |||
throw new UnknownServiceError('Could not return refs for service.'); | |||
} | |||
const repoBasePath = RepoServiceImpl.getRepoBasePath(ownerType, ownerName, repoName); | |||
return new Promise<Stream>(async (resolve, reject) => { | |||
const stream = new PassThrough({ | |||
emitClose: true, | |||
autoDestroy: true, | |||
}); | |||
const childProcess = await childProcessMethod(repoBasePath); | |||
childProcess.on('close', (code) => { | |||
if (code === 0) { | |||
resolve(stream); | |||
return; | |||
} | |||
switch (code) { | |||
case 128: | |||
reject(new RepoNotFoundError(`Repo "${repoBasePath}" does not exist. Check that the repo has been created and not deleted previously.`)); | |||
return; | |||
default: | |||
break; | |||
} | |||
reject(new UnableToInvokeRepoServiceError(`Something went wrong while invoking repo service: Git process received error code ${code}.`)); | |||
}); | |||
stream.push(`${RepoServiceImpl.gitPackSideband(`# service=${service}\n`)}0000`); | |||
if (childProcess.stdout) { | |||
childProcess.stdout.pipe(stream); | |||
} | |||
}); | |||
} | |||
async getRefsFromService( | |||
ownerType: Repo['ownerType'], | |||
ownerName: Repo['name'], | |||
repoName: Repo['name'], | |||
service?: string, | |||
userRaw?: User, | |||
): Promise<Stream> { | |||
const user = RepoServiceImpl.authenticate(userRaw); | |||
if (!user) { | |||
throw new UnauthorizedToInvokeRepoServiceError('Could not invoke repo service with insufficient authorization.'); | |||
} | |||
// TODO compute user abilities here | |||
return this.doGetRefsFromService(ownerType, ownerName, repoName, service); | |||
} | |||
} |
@@ -0,0 +1,4 @@ | |||
export * from './Repo.service' | |||
export * from './Repo.controller' | |||
export * from './models'; | |||
export * from './responses'; |
@@ -0,0 +1,30 @@ | |||
import {Org} from '../org'; | |||
import {User} from '../user'; | |||
export enum RepoVisibility { | |||
PRIVATE = 'PRIVATE', | |||
INTERNAL = 'INTERNAL', | |||
PUBLIC = 'PUBLIC', | |||
} | |||
export type Owner = User | Org; | |||
export enum RepoAction { | |||
REPO_CREATED = 'REPO_CREATED', | |||
REPO_REMOVED = 'REPO_REMOVED', | |||
} | |||
export enum RepoAvailableService { | |||
UPLOAD_PACK = 'git-upload-pack', | |||
RECEIVE_PACK = 'git-receive-pack', | |||
} | |||
export type Repo = { | |||
id: Buffer, | |||
name: string, | |||
ownerId: Buffer, | |||
ownerType: string, | |||
visibility: string, | |||
} | |||
export type CreateRepoData = Omit<Repo, 'id'>; |
@@ -0,0 +1,11 @@ | |||
import {constants} from 'http2'; | |||
import {HttpError} from '../../packages/fastify-compliant-http-errors'; | |||
export class UnauthorizedToCreateRepoError extends HttpError(constants.HTTP_STATUS_UNAUTHORIZED, 'Unauthorized to Create Repo') {} | |||
export class UnauthorizedToDeleteRepoError extends HttpError(constants.HTTP_STATUS_UNAUTHORIZED, 'Unauthorized to Delete Repo') {} | |||
export class UnauthorizedToInvokeRepoServiceError extends HttpError(constants.HTTP_STATUS_UNAUTHORIZED, 'Unauthorized to Invoke Repo Service') {} | |||
export class UnableToCreateRepoError extends HttpError(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, 'Unable to Create Repo') {} | |||
export class UnableToDeleteRepoError extends HttpError(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, 'Unable to Delete Repo') {} | |||
export class UnableToInvokeRepoServiceError extends HttpError(constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, 'Unable to Invoke Repo Service') {} | |||
export class UnknownServiceError extends HttpError(constants.HTTP_STATUS_BAD_REQUEST, 'Unknown Service') {} | |||
export class RepoNotFoundError extends HttpError(constants.HTTP_STATUS_NOT_FOUND, 'Repo Not Found') {} |
@@ -0,0 +1,30 @@ | |||
import {ServerInstance} from '../../packages/fastify-utils-theoryofnekomata'; | |||
import {RepoController, RepoControllerImpl} from './Repo.controller'; | |||
export const addRoutes = (server: ServerInstance) => { | |||
const repoController: RepoController = new RepoControllerImpl() | |||
server.route({ | |||
method: 'POST', | |||
url: '/api/repos', | |||
handler: repoController.createRepo, | |||
}) | |||
server.route({ | |||
method: 'DELETE', | |||
url: '/api/repos/:id', | |||
handler: repoController.deleteRepo, | |||
}) | |||
server.route({ | |||
method: 'GET', | |||
url: '/repos/:ownerType/:ownerName/:repoName/info/refs', | |||
handler: repoController.getRepoRefs, | |||
}) | |||
server.route({ | |||
method: 'POST', | |||
url: '/repos/:ownerType/:ownerName/:repoName/git-receive-pack', | |||
handler: repoController.receivePack, | |||
}) | |||
} |
@@ -0,0 +1,40 @@ | |||
import { RouteHandlerMethod } from 'fastify' | |||
import { Controller } from 'src/packages/fastify-utils-theoryofnekomata' | |||
import { UserService, UserServiceImpl } from 'src/modules/user/User.service' | |||
import { RegisterUserFormData } from 'src/modules/user/models' | |||
import { SessionService, SessionServiceImpl } from 'src/modules/auth' | |||
import { | |||
UserRegisteredResponse, | |||
UnableToRegisterUserError, | |||
} from './responses' | |||
export interface UserController extends Controller<'register'> { | |||
} | |||
export class UserControllerImpl implements UserController { | |||
private readonly userService: UserService | |||
private readonly sessionService: SessionService | |||
constructor() { | |||
this.userService = new UserServiceImpl() | |||
this.sessionService = new SessionServiceImpl() | |||
} | |||
readonly register: RouteHandlerMethod = async (request, reply) => { | |||
try { | |||
const body = request.body as RegisterUserFormData | |||
const newUser = await this.userService.create({ | |||
username: body.username, | |||
password: body.confirmNewPassword, | |||
}) | |||
const newSession = await this.sessionService.create({ | |||
userId: newUser.id, | |||
}) | |||
reply.sendData(new UserRegisteredResponse(newSession)) | |||
} catch (causeRaw) { | |||
const cause = causeRaw as Error | |||
throw new UnableToRegisterUserError('The operation could not be performed. Try again later.', { cause }) | |||
} | |||
} | |||
} |
@@ -0,0 +1,77 @@ | |||
import { PrismaClient } from '@prisma/client' | |||
import {CreateUserData, PublicUser, User} from 'src/modules/user/models'; | |||
import { PasswordService, PasswordServiceImpl, LoginUserFormData } from 'src/modules/auth' | |||
import { Uuid } from '@theoryofnekomata/uuid-buffer'; | |||
import { notFoundFactory } from '../../packages/prisma-error-utils'; | |||
import {UserNotFoundError} from './responses'; | |||
export interface UserService { | |||
create(data: CreateUserData): Promise<PublicUser>; | |||
getFromCredentials(args: LoginUserFormData): Promise<PublicUser>; | |||
getFromId(id: User['id']): Promise<PublicUser>; | |||
} | |||
export class UserServiceImpl implements UserService { | |||
private readonly prismaClient: PrismaClient | |||
private readonly passwordService: PasswordService | |||
constructor() { | |||
this.prismaClient = new PrismaClient() | |||
this.passwordService = new PasswordServiceImpl() | |||
} | |||
async create(data: CreateUserData): Promise<PublicUser> { | |||
const newUser = await this.prismaClient.user.create({ | |||
data: { | |||
...data, | |||
id: Uuid.v4(), | |||
password: await this.passwordService.hash(data.password), | |||
}, | |||
select: { | |||
id: true, | |||
username: true, | |||
password: false, | |||
}, | |||
}) | |||
return { | |||
...newUser, | |||
id: Uuid.from(newUser.id), | |||
} | |||
} | |||
async getFromCredentials(args: LoginUserFormData): Promise<PublicUser> { | |||
const { password: hashedPassword, ...existingUser } = await this.prismaClient.user.findUnique({ | |||
where: { | |||
username: args.username, | |||
}, | |||
rejectOnNotFound: notFoundFactory(UserNotFoundError), | |||
}) | |||
await this.passwordService.assertEqual(args.password, hashedPassword) | |||
return { | |||
...existingUser, | |||
id: Uuid.from(existingUser.id), | |||
} | |||
} | |||
async getFromId(id: User['id']): Promise<PublicUser> { | |||
const existingUser = await this.prismaClient.user.findUnique({ | |||
where: { | |||
id, | |||
}, | |||
select: { | |||
id: true, | |||
username: true, | |||
}, | |||
rejectOnNotFound: notFoundFactory(UserNotFoundError), | |||
}); | |||
return { | |||
...existingUser, | |||
id: Uuid.from(existingUser.id), | |||
}; | |||
} | |||
} |
@@ -0,0 +1,4 @@ | |||
export * from './models' | |||
export * from './User.controller' | |||
export * from './User.service' | |||
export * from './responses' |
@@ -0,0 +1,16 @@ | |||
export interface User { | |||
id: Buffer | |||
username: string | |||
password: string | |||
} | |||
export interface CreateUserData extends Omit<User, 'id'> { | |||
} | |||
export interface PublicUser extends Omit<User, 'password'> { | |||
} | |||
export interface RegisterUserFormData extends Pick<User, 'username'> { | |||
newPassword: string; | |||
confirmNewPassword: string; | |||
} |
@@ -0,0 +1,14 @@ | |||
import { constants } from 'http2' | |||
import { HttpResponse } from 'src/packages/fastify-send-data' | |||
import { Session } from 'src/modules/auth' | |||
import {HttpError} from 'src/packages/fastify-compliant-http-errors'; | |||
export class UserRegisteredResponse extends HttpResponse<Session>(constants.HTTP_STATUS_OK, 'User Registered') { | |||
} | |||
export class UnableToRegisterUserError extends HttpError( | |||
constants.HTTP_STATUS_INTERNAL_SERVER_ERROR, | |||
'Unable to Register', | |||
) {} | |||
export class UserNotFoundError extends HttpError(constants.HTTP_STATUS_NOT_FOUND, 'User Not Found') {} |
@@ -0,0 +1,11 @@ | |||
import {ServerInstance} from '../../packages/fastify-utils-theoryofnekomata'; | |||
import {UserControllerImpl, UserController} from './User.controller'; | |||
export const addRoutes = (server: ServerInstance) => { | |||
const userController: UserController = new UserControllerImpl() | |||
server.route({ | |||
method: 'POST', | |||
url: '/api/users/register', | |||
handler: userController.register, | |||
}); | |||
} |
@@ -0,0 +1,44 @@ | |||
import fp from 'fastify-plugin'; | |||
import {FastifyInstance} from 'fastify'; | |||
declare module 'fastify' { | |||
interface FastifyInstance { | |||
allStaticRoutes: { | |||
method: string, | |||
url: string, | |||
}[] | |||
} | |||
} | |||
const fastifyHomeRoute = async (app: FastifyInstance) => { | |||
app.decorateRequest('allStaticRoutes', null); | |||
app.addHook('onRoute', (routeData) => { | |||
if (routeData.url.includes('/:')) { | |||
return; | |||
} | |||
const mutableServer = app as unknown as Record<string, unknown>; | |||
mutableServer['allStaticRoutes'] = !Array.isArray(mutableServer['allStaticRoutes']) | |||
? [ | |||
{ | |||
method: routeData.method, | |||
url: routeData.url, | |||
} | |||
] | |||
: [ | |||
...mutableServer['allStaticRoutes'], | |||
{ | |||
method: routeData.method, | |||
url: routeData.url, | |||
}, | |||
]; | |||
}); | |||
app.addHook('onReady', () => { | |||
const mutableServer = app as unknown as Record<string, unknown>; | |||
if (!mutableServer['allStaticRoutes']) { | |||
mutableServer['allStaticRoutes'] = []; | |||
} | |||
}) | |||
} | |||
export default fp(fastifyHomeRoute); |
@@ -8,6 +8,11 @@ export interface ResponseDataInterface<T extends unknown = undefined> { | |||
body?: T extends undefined ? undefined : { data: T } | |||
} | |||
const BLANK_BODY_STATUS_CODES = [ | |||
constants.HTTP_STATUS_NO_CONTENT, | |||
constants.HTTP_STATUS_RESET_CONTENT, | |||
] | |||
const fastifySendData = async (app: FastifyInstance) => { | |||
const sendDataKey = 'sendData' as const | |||
app.decorateReply(sendDataKey, function reply<T extends ResponseDataInterface>(this: FastifyReply, data: T) { | |||
@@ -15,7 +20,7 @@ const fastifySendData = async (app: FastifyInstance) => { | |||
if (data.statusMessage) { | |||
this.raw.statusMessage = data.statusMessage | |||
} | |||
if (data.statusCode !== constants.HTTP_STATUS_NO_CONTENT) { | |||
if (!BLANK_BODY_STATUS_CODES.includes(data.statusCode)) { | |||
this.send(data.body) | |||
return | |||
} | |||
@@ -42,7 +47,7 @@ export const HttpResponse = <T extends unknown = undefined>( | |||
readonly body?: { data: T } | |||
constructor(data?: T) { | |||
if (data && statusCode !== constants.HTTP_STATUS_NO_CONTENT) { | |||
if (data && !BLANK_BODY_STATUS_CODES.includes(statusCode)) { | |||
this.body = { | |||
data, | |||
} | |||
@@ -1,23 +1,33 @@ | |||
import fp from 'fastify-plugin' | |||
import { FastifyInstance, FastifyRequest } from 'fastify' | |||
export interface FastifySessionOpts<SessionType = unknown, SessionId = string, RequestType extends FastifyRequest = FastifyRequest> { | |||
sessionRequestKey?: string, | |||
export interface FastifySessionOpts<SessionType = unknown, SessionId = string, UserType = unknown, RequestType extends FastifyRequest = FastifyRequest> { | |||
extractSessionId: (request: RequestType) => SessionId | null | undefined, | |||
isSessionValid: (sessionId: SessionId) => Promise<boolean>, | |||
getSession: (sessionId: SessionId) => Promise<SessionType> | |||
getSession: (sessionId: SessionId) => Promise<SessionType>, | |||
getUserFromSession?: (session: SessionType) => Promise<UserType>, | |||
sessionRequestKey?: string, | |||
userRequestKey?: string, | |||
} | |||
const fastifySession = async (app: FastifyInstance, opts: FastifySessionOpts) => { | |||
const { sessionRequestKey = 'session' } = opts | |||
app.decorateRequest(sessionRequestKey, null) | |||
const { | |||
sessionRequestKey = 'session', | |||
userRequestKey = 'user' | |||
} = opts; | |||
app.decorateRequest(sessionRequestKey, null); | |||
app.decorateRequest(userRequestKey, null); | |||
app.addHook('onRequest', async (request: FastifyRequest) => { | |||
const sessionId = opts.extractSessionId(request) | |||
if (typeof sessionId === 'string') { | |||
const isSessionValid = await opts.isSessionValid(sessionId) | |||
if (isSessionValid) { | |||
const mutableRequest = (request as unknown) as Record<string, unknown> | |||
mutableRequest[sessionRequestKey] = await opts.getSession(sessionId) | |||
const session = await opts.getSession(sessionId); | |||
mutableRequest[sessionRequestKey] = session; | |||
if (typeof opts.getUserFromSession === 'function') { | |||
mutableRequest[userRequestKey] = await opts.getUserFromSession(session); | |||
} | |||
} | |||
} | |||
}) | |||
@@ -0,0 +1,7 @@ | |||
import {FastifyInstance, RouteHandlerMethod} from 'fastify'; | |||
export type ServerInstance = FastifyInstance; | |||
export type Controller<T extends string> = { | |||
[key in T]: RouteHandlerMethod; | |||
}; |
@@ -0,0 +1 @@ | |||
export const notFoundFactory = <T extends Error>(ErrorClass: { new(): T }) => () => new ErrorClass() |
@@ -1,21 +1,16 @@ | |||
import { FastifyInstance } from 'fastify'; | |||
import * as git from './modules/git' | |||
import {ServerInstance} from './packages/fastify-utils-theoryofnekomata'; | |||
import * as auth from './modules/auth/routes'; | |||
import * as user from './modules/user/routes'; | |||
import * as repo from './modules/repo/routes'; | |||
export const addRoutes = (server: FastifyInstance) => { | |||
server.get('/', async (_, reply) => { | |||
reply.send({hello: 'world'}) | |||
}); | |||
const gitController = new git.GitControllerImpl() | |||
server.route({ | |||
method: 'POST', | |||
url: '/repos', | |||
handler: gitController.createRepo, | |||
}) | |||
export const addRoutes = (server: ServerInstance) => { | |||
auth.addRoutes(server); | |||
user.addRoutes(server); | |||
repo.addRoutes(server); | |||
server.route({ | |||
method: 'DELETE', | |||
url: '/repos/:id', | |||
handler: gitController.deleteRepo, | |||
}) | |||
server.get('/', async (request, reply) => { | |||
reply.send({ | |||
routes: request.server.allStaticRoutes, | |||
}); | |||
}); | |||
} |
@@ -1,20 +1,144 @@ | |||
import fastify, {FastifyServerOptions} from 'fastify'; | |||
import * as codeCore from '@modal/code-core'; | |||
import fastify, {FastifyRequest, FastifyServerOptions} from 'fastify'; | |||
import {fastifyErrorHandler} from './packages/fastify-compliant-http-errors'; | |||
import fastifySendData from './packages/fastify-send-data'; | |||
import fastifyServiceSession from './packages/fastify-service-session'; | |||
import {Session, SessionService, SessionServiceImpl} from './modules/auth'; | |||
import {Uuid} from '@theoryofnekomata/uuid-buffer'; | |||
import fastifyHomeRoute from './packages/fastify-home-route'; | |||
import {User} from './modules/user'; | |||
import {IncomingMessage} from 'http'; | |||
import {createDeflate, createGunzip} from 'zlib'; | |||
import {PassThrough, Transform} from 'stream'; | |||
interface RequestLike extends Pick<FastifyRequest, 'query' | 'headers' | 'body'> {} | |||
declare module 'fastify' { | |||
interface FastifyRequest { | |||
user?: codeCore.common.User, | |||
session?: Session & { user: User }, | |||
} | |||
} | |||
enum AvailableDecoder { | |||
GZIP = 'gzip', | |||
DEFLATE = 'deflate', | |||
} | |||
type DecoderFunction = () => Transform; | |||
const DECODERS: Record<AvailableDecoder, DecoderFunction> = { | |||
[AvailableDecoder.GZIP]:() => createGunzip(), | |||
[AvailableDecoder.DEFLATE]: () => createDeflate(), | |||
} | |||
const DEFAULT_DECODER: DecoderFunction = () => new PassThrough(); | |||
class PackRequestTransformStream extends Transform { | |||
private data: Record<string, unknown> = { | |||
last: null, | |||
commit: null, | |||
ref: null, | |||
} | |||
constructor() { | |||
super(); | |||
const headerRegExp = /([0-9a-fA-F]+) ([0-9a-fA-F]+) (.+?)( |00|\u0000)|^(0000)$/gi; | |||
let bufferedData = Buffer.from(''); | |||
this.on('data', (chunk) => { | |||
const isHeaderRead = this.data.last && this.data.commit; | |||
if (!isHeaderRead) { | |||
bufferedData = Buffer.concat([bufferedData, chunk]); | |||
const bufferAsString = bufferedData.toString('utf-8'); | |||
const bufferHeaderMatch = bufferAsString.match(headerRegExp); | |||
if (bufferHeaderMatch) { | |||
const [ | |||
_header, | |||
last, | |||
commit, | |||
ref, | |||
] = Array.from(bufferHeaderMatch); | |||
this.data = { | |||
last, | |||
commit, | |||
ref, | |||
} | |||
} | |||
} | |||
this.push(chunk); | |||
}) | |||
} | |||
} | |||
const receivePackRequestParser = ( | |||
request: FastifyRequest, | |||
payload: IncomingMessage, | |||
done: Function, | |||
) => { | |||
console.log(request.headers); | |||
const encoding = request.headers['content-encoding']; | |||
let theDecoder: DecoderFunction = DEFAULT_DECODER; | |||
if (encoding) { | |||
const { [encoding as AvailableDecoder]: decoder } = DECODERS; | |||
if (!decoder) { | |||
done(new Error(`Unknown encoding: ${encoding}`)); | |||
return; | |||
} | |||
theDecoder = decoder; | |||
} | |||
const requestParserStream = new PackRequestTransformStream(); | |||
const pipeline = payload | |||
.pipe(theDecoder()) | |||
.pipe(requestParserStream) | |||
pipeline.on('end', () => { | |||
done(null, pipeline); | |||
}); | |||
}; | |||
export const createServer = (opts?: FastifyServerOptions) => { | |||
const server = fastify(opts) | |||
const server = fastify(opts); | |||
server.setErrorHandler(fastifyErrorHandler) | |||
server.register(fastifyHomeRoute); | |||
server.addContentTypeParser('application/x-git-receive-pack-request', receivePackRequestParser); | |||
server.addContentTypeParser('application/x-git-upload-pack-request', receivePackRequestParser); // TODO | |||
server.register(fastifyServiceSession, { | |||
sessionRequestKey: 'session', | |||
getSession: async (sessionId: string) => { | |||
const sessionService: SessionService = new SessionServiceImpl() | |||
return sessionService.get(Uuid.from(sessionId)) | |||
}, | |||
isSessionValid: async (sessionId: string) => { | |||
const sessionService: SessionService = new SessionServiceImpl() | |||
return sessionService.isValid(Uuid.from(sessionId)) | |||
}, | |||
extractSessionId: (request: RequestLike) => { | |||
const sessionIdKey = 'sessionId' | |||
const query = request.query as Record<string, unknown> | |||
const querySessionId = query[sessionIdKey] | |||
if (typeof querySessionId === 'string') { | |||
return querySessionId | |||
} | |||
const { authorization } = request.headers | |||
if (typeof authorization === 'string') { | |||
const [authType, headerSessionId] = authorization.split(' ') | |||
if (authType === 'Bearer') { | |||
return headerSessionId | |||
} | |||
} | |||
const body = request.body as Record<string, unknown> ?? {} | |||
const bodySessionId = body[sessionIdKey] | |||
if (typeof bodySessionId === 'string') { | |||
return bodySessionId | |||
} | |||
server.register(fastifySendData) | |||
return undefined | |||
} | |||
}) | |||
server.register(fastifySendData); | |||
return server; | |||
} |
@@ -1,7 +0,0 @@ | |||
import { RouteHandlerMethod } from 'fastify' | |||
type Controller<T extends string> = { | |||
[key in T]: RouteHandlerMethod; | |||
}; | |||
export default Controller |
@@ -7,7 +7,7 @@ | |||
"importHelpers": true, | |||
"declaration": true, | |||
"sourceMap": true, | |||
"rootDir": "./", | |||
"baseUrl": ".", | |||
"strict": true, | |||
"noUnusedLocals": true, | |||
"noUnusedParameters": true, | |||
@@ -7,7 +7,7 @@ | |||
"importHelpers": true, | |||
"declaration": true, | |||
"sourceMap": true, | |||
"rootDir": "./src", | |||
"baseUrl": ".", | |||
"strict": true, | |||
"noUnusedLocals": true, | |||
"noUnusedParameters": true, | |||
@@ -316,6 +316,21 @@ | |||
"@jridgewell/resolve-uri" "^3.0.3" | |||
"@jridgewell/sourcemap-codec" "^1.4.10" | |||
"@mapbox/node-pre-gyp@^1.0.0": | |||
version "1.0.9" | |||
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.9.tgz#09a8781a3a036151cdebbe8719d6f8b25d4058bc" | |||
integrity sha512-aDF3S3rK9Q2gey/WAttUlISduDItz5BU3306M9Eyv6/oS40aMprnopshtlKTykxRNIBEZuRMaZAnbrQ4QtKGyw== | |||
dependencies: | |||
detect-libc "^2.0.0" | |||
https-proxy-agent "^5.0.0" | |||
make-dir "^3.1.0" | |||
node-fetch "^2.6.7" | |||
nopt "^5.0.0" | |||
npmlog "^5.0.1" | |||
rimraf "^3.0.2" | |||
semver "^7.3.5" | |||
tar "^6.1.11" | |||
"@mdn/browser-compat-data@^3.3.14": | |||
version "3.3.14" | |||
resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-3.3.14.tgz#b72a37c654e598f9ae6f8335faaee182bebc6b28" | |||
@@ -359,6 +374,23 @@ | |||
resolved "https://registry.yarnpkg.com/@ovyerus/licenses/-/licenses-6.4.4.tgz#596e3ace46ab7c70bcf0e2b17f259796a4bedf9f" | |||
integrity sha512-IHjc31WXciQT3hfvdY+M59jBkQp70Fpr04tNDVO5rez2PNv4u8tE6w//CkU+GeBoO9k2ahneSqzjzvlgjyjkGw== | |||
"@prisma/client@^3.14.0": | |||
version "3.14.0" | |||
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.14.0.tgz#bb90405c012fcca11f4647d91153ed4c58f3bd48" | |||
integrity sha512-atb41UpgTR1MCst0VIbiHTMw8lmXnwUvE1KyUCAkq08+wJyjRE78Due+nSf+7uwqQn+fBFYVmoojtinhlLOSaA== | |||
dependencies: | |||
"@prisma/engines-version" "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" | |||
"@prisma/engines-version@3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a": | |||
version "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" | |||
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a.tgz#4edae57cf6527f35e22cebe75e49214fc0e99ac9" | |||
integrity sha512-D+yHzq4a2r2Rrd0ZOW/mTZbgDIkUkD8ofKgusEI1xPiZz60Daks+UM7Me2ty5FzH3p/TgyhBpRrfIHx+ha20RQ== | |||
"@prisma/engines@3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a": | |||
version "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" | |||
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a.tgz#7fa11bc26a51d450185c816cc0ab8cac673fb4bf" | |||
integrity sha512-LwZvI3FY6f43xFjQNRuE10JM5R8vJzFTSmbV9X0Wuhv9kscLkjRlZt0BEoiHmO+2HA3B3xxbMfB5du7ZoSFXGg== | |||
"@theoryofnekomata/uuid-buffer@^0.1.0": | |||
version "0.1.0" | |||
resolved "https://js.pack.modal.sh/@theoryofnekomata%2fuuid-buffer/-/uuid-buffer-0.1.0.tgz#0917314e8230ce1a2047172b3277512bc0fd73a3" | |||
@@ -367,6 +399,13 @@ | |||
"@types/uuid" "^8.3.4" | |||
uuid "^8.3.2" | |||
"@types/bcrypt@^5.0.0": | |||
version "5.0.0" | |||
resolved "https://registry.yarnpkg.com/@types/bcrypt/-/bcrypt-5.0.0.tgz#a835afa2882d165aff5690893db314eaa98b9f20" | |||
integrity sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw== | |||
dependencies: | |||
"@types/node" "*" | |||
"@types/chai-subset@^1.3.3": | |||
version "1.3.3" | |||
resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.3.tgz#97893814e92abd2c534de422cb377e0e0bdaac94" | |||
@@ -389,7 +428,7 @@ | |||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" | |||
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== | |||
"@types/node@^17.0.25": | |||
"@types/node@*", "@types/node@^17.0.25": | |||
version "17.0.35" | |||
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.35.tgz#635b7586086d51fb40de0a2ec9d1014a5283ba4a" | |||
integrity sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg== | |||
@@ -486,6 +525,11 @@ | |||
"@typescript-eslint/types" "5.25.0" | |||
eslint-visitor-keys "^3.3.0" | |||
abbrev@1: | |||
version "1.1.1" | |||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" | |||
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== | |||
abstract-logging@^2.0.0: | |||
version "2.0.1" | |||
resolved "https://registry.yarnpkg.com/abstract-logging/-/abstract-logging-2.0.1.tgz#6b0c371df212db7129b57d2e7fcf282b8bf1c839" | |||
@@ -501,6 +545,13 @@ acorn@^8.7.1: | |||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" | |||
integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== | |||
agent-base@6: | |||
version "6.0.2" | |||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" | |||
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== | |||
dependencies: | |||
debug "4" | |||
ajv@^6.10.0, ajv@^6.11.0, ajv@^6.12.4, ajv@^6.12.6: | |||
version "6.12.6" | |||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" | |||
@@ -545,11 +596,24 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: | |||
dependencies: | |||
color-convert "^2.0.1" | |||
"aproba@^1.0.3 || ^2.0.0": | |||
version "2.0.0" | |||
resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" | |||
integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== | |||
archy@^1.0.0: | |||
version "1.0.0" | |||
resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" | |||
integrity sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw== | |||
are-we-there-yet@^2.0.0: | |||
version "2.0.0" | |||
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" | |||
integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== | |||
dependencies: | |||
delegates "^1.0.0" | |||
readable-stream "^3.6.0" | |||
argparse@^2.0.1: | |||
version "2.0.1" | |||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" | |||
@@ -651,6 +715,14 @@ base64-js@^1.3.1: | |||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" | |||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== | |||
bcrypt@^5.0.1: | |||
version "5.0.1" | |||
resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.0.1.tgz#f1a2c20f208e2ccdceea4433df0c8b2c54ecdf71" | |||
integrity sha512-9BTgmrhZM2t1bNuDtrtIMVSmmxZBrJ71n8Wg+YgdjHuIWYF7SjjmCPZFB+/5i/o/PIeRpwVJR3P+NrpIItUjqw== | |||
dependencies: | |||
"@mapbox/node-pre-gyp" "^1.0.0" | |||
node-addon-api "^3.1.0" | |||
bl@^5.0.0: | |||
version "5.0.0" | |||
resolved "https://registry.yarnpkg.com/bl/-/bl-5.0.0.tgz#6928804a41e9da9034868e1c50ca88f21f57aea2" | |||
@@ -762,6 +834,11 @@ check-error@^1.0.2: | |||
resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" | |||
integrity sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA== | |||
chownr@^2.0.0: | |||
version "2.0.0" | |||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" | |||
integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== | |||
cli-cursor@^4.0.0: | |||
version "4.0.0" | |||
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-4.0.0.tgz#3cecfe3734bf4fe02a8361cbdc0f6fe28c6a57ea" | |||
@@ -821,6 +898,11 @@ color-name@~1.1.4: | |||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" | |||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== | |||
color-support@^1.1.2: | |||
version "1.1.3" | |||
resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" | |||
integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== | |||
concat-map@0.0.1: | |||
version "0.0.1" | |||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" | |||
@@ -843,6 +925,11 @@ confusing-browser-globals@^1.0.10: | |||
resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81" | |||
integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA== | |||
console-control-strings@^1.0.0, console-control-strings@^1.1.0: | |||
version "1.1.0" | |||
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" | |||
integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= | |||
convert-source-map@^1.7.0: | |||
version "1.8.0" | |||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" | |||
@@ -889,6 +976,13 @@ damerau-levenshtein@^1.0.7: | |||
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" | |||
integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== | |||
debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: | |||
version "4.3.4" | |||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" | |||
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== | |||
dependencies: | |||
ms "2.1.2" | |||
debug@^2.6.9: | |||
version "2.6.9" | |||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" | |||
@@ -903,13 +997,6 @@ debug@^3.2.7: | |||
dependencies: | |||
ms "^2.1.1" | |||
debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: | |||
version "4.3.4" | |||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" | |||
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== | |||
dependencies: | |||
ms "2.1.2" | |||
decamelize@^1.2.0: | |||
version "1.2.0" | |||
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" | |||
@@ -952,11 +1039,21 @@ degit@^2.8.4: | |||
resolved "https://registry.yarnpkg.com/degit/-/degit-2.8.4.tgz#3bb9c5c00f157c44724dd4a50724e4aa75a54d38" | |||
integrity sha512-vqYuzmSA5I50J882jd+AbAhQtgK6bdKUJIex1JNfEUPENCgYsxugzKVZlFyMwV4i06MmnV47/Iqi5Io86zf3Ng== | |||
delegates@^1.0.0: | |||
version "1.0.0" | |||
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" | |||
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= | |||
detect-indent@^6.0.0: | |||
version "6.1.0" | |||
resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" | |||
integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== | |||
detect-libc@^2.0.0: | |||
version "2.0.1" | |||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" | |||
integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== | |||
dir-glob@^3.0.1: | |||
version "3.0.1" | |||
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" | |||
@@ -1656,6 +1753,13 @@ fs-extra@^10.0.0: | |||
jsonfile "^6.0.1" | |||
universalify "^2.0.0" | |||
fs-minipass@^2.0.0: | |||
version "2.1.0" | |||
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" | |||
integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== | |||
dependencies: | |||
minipass "^3.0.0" | |||
fs.realpath@^1.0.0: | |||
version "1.0.0" | |||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" | |||
@@ -1696,6 +1800,21 @@ fuzzy-search@^3.2.1: | |||
resolved "https://registry.yarnpkg.com/fuzzy-search/-/fuzzy-search-3.2.1.tgz#65d5faad6bc633aee86f1898b7788dfe312ac6c9" | |||
integrity sha512-vAcPiyomt1ioKAsAL2uxSABHJ4Ju/e4UeDM+g1OlR0vV4YhLGMNsdLNvZTpEDY4JCSt0E4hASCNM5t2ETtsbyg== | |||
gauge@^3.0.0: | |||
version "3.0.2" | |||
resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" | |||
integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== | |||
dependencies: | |||
aproba "^1.0.3 || ^2.0.0" | |||
color-support "^1.1.2" | |||
console-control-strings "^1.0.0" | |||
has-unicode "^2.0.1" | |||
object-assign "^4.1.1" | |||
signal-exit "^3.0.0" | |||
string-width "^4.2.3" | |||
strip-ansi "^6.0.1" | |||
wide-align "^1.1.2" | |||
gensync@^1.0.0-beta.2: | |||
version "1.0.0-beta.2" | |||
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" | |||
@@ -1839,6 +1958,11 @@ has-tostringtag@^1.0.0: | |||
dependencies: | |||
has-symbols "^1.0.2" | |||
has-unicode@^2.0.1: | |||
version "2.0.1" | |||
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" | |||
integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= | |||
has@^1.0.3: | |||
version "1.0.3" | |||
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" | |||
@@ -1846,6 +1970,14 @@ has@^1.0.3: | |||
dependencies: | |||
function-bind "^1.1.1" | |||
https-proxy-agent@^5.0.0: | |||
version "5.0.1" | |||
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" | |||
integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== | |||
dependencies: | |||
agent-base "6" | |||
debug "4" | |||
human-signals@^2.1.0: | |||
version "2.1.0" | |||
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" | |||
@@ -2222,7 +2354,7 @@ lru-cache@^6.0.0: | |||
dependencies: | |||
yallist "^4.0.0" | |||
make-dir@^3.0.0: | |||
make-dir@^3.0.0, make-dir@^3.1.0: | |||
version "3.1.0" | |||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" | |||
integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== | |||
@@ -2264,6 +2396,26 @@ minimist@^1.2.0, minimist@^1.2.6: | |||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" | |||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== | |||
minipass@^3.0.0: | |||
version "3.1.6" | |||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee" | |||
integrity sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ== | |||
dependencies: | |||
yallist "^4.0.0" | |||
minizlib@^2.1.1: | |||
version "2.1.2" | |||
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" | |||
integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== | |||
dependencies: | |||
minipass "^3.0.0" | |||
yallist "^4.0.0" | |||
mkdirp@^1.0.3: | |||
version "1.0.4" | |||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" | |||
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== | |||
ms@2.0.0: | |||
version "2.0.0" | |||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" | |||
@@ -2289,11 +2441,30 @@ natural-compare@^1.4.0: | |||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" | |||
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= | |||
node-addon-api@^3.1.0: | |||
version "3.2.1" | |||
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" | |||
integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== | |||
node-fetch@^2.6.7: | |||
version "2.6.7" | |||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" | |||
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== | |||
dependencies: | |||
whatwg-url "^5.0.0" | |||
node-releases@^2.0.3: | |||
version "2.0.4" | |||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.4.tgz#f38252370c43854dc48aa431c766c6c398f40476" | |||
integrity sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ== | |||
nopt@^5.0.0: | |||
version "5.0.0" | |||
resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" | |||
integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== | |||
dependencies: | |||
abbrev "1" | |||
npm-run-path@^4.0.1: | |||
version "4.0.1" | |||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" | |||
@@ -2301,6 +2472,16 @@ npm-run-path@^4.0.1: | |||
dependencies: | |||
path-key "^3.0.0" | |||
npmlog@^5.0.1: | |||
version "5.0.1" | |||
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" | |||
integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== | |||
dependencies: | |||
are-we-there-yet "^2.0.0" | |||
console-control-strings "^1.1.0" | |||
gauge "^3.0.0" | |||
set-blocking "^2.0.0" | |||
nth-check@^2.0.1: | |||
version "2.0.1" | |||
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2" | |||
@@ -2584,6 +2765,13 @@ pridepack@1.1.1: | |||
resolve.exports "^1.1.0" | |||
yargs "^17.2.1" | |||
prisma@^3.14.0: | |||
version "3.14.0" | |||
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.14.0.tgz#dd67ece37d7b5373e9fd9588971de0024b49be81" | |||
integrity sha512-l9MOgNCn/paDE+i1K2fp9NZ+Du4trzPTJsGkaQHVBufTGqzoYHuNk8JfzXuIn0Gte6/ZjyKj652Jq/Lc1tp2yw== | |||
dependencies: | |||
"@prisma/engines" "3.14.0-36.2b0c12756921c891fec4f68d9444e18c7d5d4a6a" | |||
process-warning@^1.0.0: | |||
version "1.0.0" | |||
resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-1.0.0.tgz#980a0b25dc38cd6034181be4b7726d89066b4616" | |||
@@ -2634,7 +2822,7 @@ react-is@^16.13.1: | |||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" | |||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== | |||
readable-stream@^3.4.0: | |||
readable-stream@^3.4.0, readable-stream@^3.6.0: | |||
version "3.6.0" | |||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" | |||
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== | |||
@@ -2825,7 +3013,7 @@ side-channel@^1.0.4: | |||
get-intrinsic "^1.0.2" | |||
object-inspect "^1.9.0" | |||
signal-exit@^3.0.2, signal-exit@^3.0.3: | |||
signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: | |||
version "3.0.7" | |||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" | |||
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== | |||
@@ -2858,7 +3046,7 @@ string-similarity@^4.0.1: | |||
resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.4.tgz#42d01ab0b34660ea8a018da8f56a3309bb8b2a5b" | |||
integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ== | |||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: | |||
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: | |||
version "4.2.3" | |||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" | |||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== | |||
@@ -2954,6 +3142,18 @@ supports-preserve-symlinks-flag@^1.0.0: | |||
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" | |||
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== | |||
tar@^6.1.11: | |||
version "6.1.11" | |||
resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621" | |||
integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== | |||
dependencies: | |||
chownr "^2.0.0" | |||
fs-minipass "^2.0.0" | |||
minipass "^3.0.0" | |||
minizlib "^2.1.1" | |||
mkdirp "^1.0.3" | |||
yallist "^4.0.0" | |||
text-table@^0.2.0: | |||
version "0.2.0" | |||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" | |||
@@ -2986,6 +3186,11 @@ to-regex-range@^5.0.1: | |||
dependencies: | |||
is-number "^7.0.0" | |||
tr46@~0.0.3: | |||
version "0.0.3" | |||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" | |||
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= | |||
tsconfig-paths@^3.14.1: | |||
version "3.14.1" | |||
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" | |||
@@ -3131,6 +3336,19 @@ wcwidth@^1.0.1: | |||
dependencies: | |||
defaults "^1.0.3" | |||
webidl-conversions@^3.0.0: | |||
version "3.0.1" | |||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" | |||
integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= | |||
whatwg-url@^5.0.0: | |||
version "5.0.0" | |||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" | |||
integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= | |||
dependencies: | |||
tr46 "~0.0.3" | |||
webidl-conversions "^3.0.0" | |||
which-boxed-primitive@^1.0.2: | |||
version "1.0.2" | |||
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" | |||
@@ -3154,6 +3372,13 @@ which@^2.0.1: | |||
dependencies: | |||
isexe "^2.0.0" | |||
wide-align@^1.1.2: | |||
version "1.1.5" | |||
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" | |||
integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== | |||
dependencies: | |||
string-width "^1.0.2 || 2 || 3 || 4" | |||
word-wrap@^1.2.3: | |||
version "1.2.3" | |||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" | |||