The endpoints for creation, deletion, as well as refs advertisement have been implemented.master
@@ -105,3 +105,6 @@ dist | |||||
.tern-port | .tern-port | ||||
.npmrc | .npmrc | ||||
.database/ | |||||
.idea/ | |||||
repos/ |
@@ -48,6 +48,7 @@ | |||||
"pridepack" | "pridepack" | ||||
], | ], | ||||
"devDependencies": { | "devDependencies": { | ||||
"@types/bcrypt": "^5.0.0", | |||||
"@types/node": "^17.0.25", | "@types/node": "^17.0.25", | ||||
"eslint": "^8.13.0", | "eslint": "^8.13.0", | ||||
"eslint-config-lxsmnsyc": "^0.4.0", | "eslint-config-lxsmnsyc": "^0.4.0", | ||||
@@ -57,9 +58,12 @@ | |||||
"vitest": "^0.9.4" | "vitest": "^0.9.4" | ||||
}, | }, | ||||
"dependencies": { | "dependencies": { | ||||
"@prisma/client": "^3.14.0", | |||||
"@theoryofnekomata/uuid-buffer": "^0.1.0", | "@theoryofnekomata/uuid-buffer": "^0.1.0", | ||||
"bcrypt": "^5.0.1", | |||||
"fastify": "^3.27.0", | "fastify": "^3.27.0", | ||||
"fastify-plugin": "^3.0.1" | |||||
"fastify-plugin": "^3.0.1", | |||||
"prisma": "^3.14.0" | |||||
}, | }, | ||||
"scripts": { | "scripts": { | ||||
"prepublishOnly": "pridepack clean && pridepack build", | "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 { createServer } from './server'; | ||||
import { addRoutes } from './routes'; | import { addRoutes } from './routes'; | ||||
const server = createServer(); | |||||
const server = createServer({ | |||||
logger: true, | |||||
}); | |||||
addRoutes(server); | addRoutes(server); | ||||
server.listen(process.env.PORT ?? 8080, '0.0.0.0', (err) => { | 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 } | 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 fastifySendData = async (app: FastifyInstance) => { | ||||
const sendDataKey = 'sendData' as const | const sendDataKey = 'sendData' as const | ||||
app.decorateReply(sendDataKey, function reply<T extends ResponseDataInterface>(this: FastifyReply, data: T) { | app.decorateReply(sendDataKey, function reply<T extends ResponseDataInterface>(this: FastifyReply, data: T) { | ||||
@@ -15,7 +20,7 @@ const fastifySendData = async (app: FastifyInstance) => { | |||||
if (data.statusMessage) { | if (data.statusMessage) { | ||||
this.raw.statusMessage = 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) | this.send(data.body) | ||||
return | return | ||||
} | } | ||||
@@ -42,7 +47,7 @@ export const HttpResponse = <T extends unknown = undefined>( | |||||
readonly body?: { data: T } | readonly body?: { data: T } | ||||
constructor(data?: T) { | constructor(data?: T) { | ||||
if (data && statusCode !== constants.HTTP_STATUS_NO_CONTENT) { | |||||
if (data && !BLANK_BODY_STATUS_CODES.includes(statusCode)) { | |||||
this.body = { | this.body = { | ||||
data, | data, | ||||
} | } | ||||
@@ -1,23 +1,33 @@ | |||||
import fp from 'fastify-plugin' | import fp from 'fastify-plugin' | ||||
import { FastifyInstance, FastifyRequest } from 'fastify' | 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, | extractSessionId: (request: RequestType) => SessionId | null | undefined, | ||||
isSessionValid: (sessionId: SessionId) => Promise<boolean>, | 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 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) => { | app.addHook('onRequest', async (request: FastifyRequest) => { | ||||
const sessionId = opts.extractSessionId(request) | const sessionId = opts.extractSessionId(request) | ||||
if (typeof sessionId === 'string') { | if (typeof sessionId === 'string') { | ||||
const isSessionValid = await opts.isSessionValid(sessionId) | const isSessionValid = await opts.isSessionValid(sessionId) | ||||
if (isSessionValid) { | if (isSessionValid) { | ||||
const mutableRequest = (request as unknown) as Record<string, unknown> | 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 {fastifyErrorHandler} from './packages/fastify-compliant-http-errors'; | ||||
import fastifySendData from './packages/fastify-send-data'; | 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' { | declare module 'fastify' { | ||||
interface FastifyRequest { | 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) => { | export const createServer = (opts?: FastifyServerOptions) => { | ||||
const server = fastify(opts) | |||||
const server = fastify(opts); | |||||
server.setErrorHandler(fastifyErrorHandler) | 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; | 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, | "importHelpers": true, | ||||
"declaration": true, | "declaration": true, | ||||
"sourceMap": true, | "sourceMap": true, | ||||
"rootDir": "./", | |||||
"baseUrl": ".", | |||||
"strict": true, | "strict": true, | ||||
"noUnusedLocals": true, | "noUnusedLocals": true, | ||||
"noUnusedParameters": true, | "noUnusedParameters": true, | ||||
@@ -7,7 +7,7 @@ | |||||
"importHelpers": true, | "importHelpers": true, | ||||
"declaration": true, | "declaration": true, | ||||
"sourceMap": true, | "sourceMap": true, | ||||
"rootDir": "./src", | |||||
"baseUrl": ".", | |||||
"strict": true, | "strict": true, | ||||
"noUnusedLocals": true, | "noUnusedLocals": true, | ||||
"noUnusedParameters": true, | "noUnusedParameters": true, | ||||
@@ -316,6 +316,21 @@ | |||||
"@jridgewell/resolve-uri" "^3.0.3" | "@jridgewell/resolve-uri" "^3.0.3" | ||||
"@jridgewell/sourcemap-codec" "^1.4.10" | "@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": | "@mdn/browser-compat-data@^3.3.14": | ||||
version "3.3.14" | version "3.3.14" | ||||
resolved "https://registry.yarnpkg.com/@mdn/browser-compat-data/-/browser-compat-data-3.3.14.tgz#b72a37c654e598f9ae6f8335faaee182bebc6b28" | 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" | resolved "https://registry.yarnpkg.com/@ovyerus/licenses/-/licenses-6.4.4.tgz#596e3ace46ab7c70bcf0e2b17f259796a4bedf9f" | ||||
integrity sha512-IHjc31WXciQT3hfvdY+M59jBkQp70Fpr04tNDVO5rez2PNv4u8tE6w//CkU+GeBoO9k2ahneSqzjzvlgjyjkGw== | 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": | "@theoryofnekomata/uuid-buffer@^0.1.0": | ||||
version "0.1.0" | version "0.1.0" | ||||
resolved "https://js.pack.modal.sh/@theoryofnekomata%2fuuid-buffer/-/uuid-buffer-0.1.0.tgz#0917314e8230ce1a2047172b3277512bc0fd73a3" | 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" | "@types/uuid" "^8.3.4" | ||||
uuid "^8.3.2" | 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": | "@types/chai-subset@^1.3.3": | ||||
version "1.3.3" | version "1.3.3" | ||||
resolved "https://registry.yarnpkg.com/@types/chai-subset/-/chai-subset-1.3.3.tgz#97893814e92abd2c534de422cb377e0e0bdaac94" | 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" | resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" | ||||
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== | integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== | ||||
"@types/node@^17.0.25": | |||||
"@types/node@*", "@types/node@^17.0.25": | |||||
version "17.0.35" | version "17.0.35" | ||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.35.tgz#635b7586086d51fb40de0a2ec9d1014a5283ba4a" | resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.35.tgz#635b7586086d51fb40de0a2ec9d1014a5283ba4a" | ||||
integrity sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg== | integrity sha512-vu1SrqBjbbZ3J6vwY17jBs8Sr/BKA+/a/WtjRG+whKg1iuLFOosq872EXS0eXWILdO36DHQQeku/ZcL6hz2fpg== | ||||
@@ -486,6 +525,11 @@ | |||||
"@typescript-eslint/types" "5.25.0" | "@typescript-eslint/types" "5.25.0" | ||||
eslint-visitor-keys "^3.3.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: | abstract-logging@^2.0.0: | ||||
version "2.0.1" | version "2.0.1" | ||||
resolved "https://registry.yarnpkg.com/abstract-logging/-/abstract-logging-2.0.1.tgz#6b0c371df212db7129b57d2e7fcf282b8bf1c839" | 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" | resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30" | ||||
integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== | 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: | ajv@^6.10.0, ajv@^6.11.0, ajv@^6.12.4, ajv@^6.12.6: | ||||
version "6.12.6" | version "6.12.6" | ||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" | 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: | dependencies: | ||||
color-convert "^2.0.1" | 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: | archy@^1.0.0: | ||||
version "1.0.0" | version "1.0.0" | ||||
resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" | resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" | ||||
integrity sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw== | 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: | argparse@^2.0.1: | ||||
version "2.0.1" | version "2.0.1" | ||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" | 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" | resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" | ||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== | 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: | bl@^5.0.0: | ||||
version "5.0.0" | version "5.0.0" | ||||
resolved "https://registry.yarnpkg.com/bl/-/bl-5.0.0.tgz#6928804a41e9da9034868e1c50ca88f21f57aea2" | 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" | resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" | ||||
integrity sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA== | 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: | cli-cursor@^4.0.0: | ||||
version "4.0.0" | version "4.0.0" | ||||
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-4.0.0.tgz#3cecfe3734bf4fe02a8361cbdc0f6fe28c6a57ea" | 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" | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" | ||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== | 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: | concat-map@0.0.1: | ||||
version "0.0.1" | version "0.0.1" | ||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" | 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" | resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81" | ||||
integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA== | 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: | convert-source-map@^1.7.0: | ||||
version "1.8.0" | version "1.8.0" | ||||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" | 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" | resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" | ||||
integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== | 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: | debug@^2.6.9: | ||||
version "2.6.9" | version "2.6.9" | ||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" | ||||
@@ -903,13 +997,6 @@ debug@^3.2.7: | |||||
dependencies: | dependencies: | ||||
ms "^2.1.1" | 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: | decamelize@^1.2.0: | ||||
version "1.2.0" | version "1.2.0" | ||||
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" | 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" | resolved "https://registry.yarnpkg.com/degit/-/degit-2.8.4.tgz#3bb9c5c00f157c44724dd4a50724e4aa75a54d38" | ||||
integrity sha512-vqYuzmSA5I50J882jd+AbAhQtgK6bdKUJIex1JNfEUPENCgYsxugzKVZlFyMwV4i06MmnV47/Iqi5Io86zf3Ng== | 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: | detect-indent@^6.0.0: | ||||
version "6.1.0" | version "6.1.0" | ||||
resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" | resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" | ||||
integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== | 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: | dir-glob@^3.0.1: | ||||
version "3.0.1" | version "3.0.1" | ||||
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" | 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" | jsonfile "^6.0.1" | ||||
universalify "^2.0.0" | 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: | fs.realpath@^1.0.0: | ||||
version "1.0.0" | version "1.0.0" | ||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" | 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" | resolved "https://registry.yarnpkg.com/fuzzy-search/-/fuzzy-search-3.2.1.tgz#65d5faad6bc633aee86f1898b7788dfe312ac6c9" | ||||
integrity sha512-vAcPiyomt1ioKAsAL2uxSABHJ4Ju/e4UeDM+g1OlR0vV4YhLGMNsdLNvZTpEDY4JCSt0E4hASCNM5t2ETtsbyg== | 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: | gensync@^1.0.0-beta.2: | ||||
version "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" | resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" | ||||
@@ -1839,6 +1958,11 @@ has-tostringtag@^1.0.0: | |||||
dependencies: | dependencies: | ||||
has-symbols "^1.0.2" | 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: | has@^1.0.3: | ||||
version "1.0.3" | version "1.0.3" | ||||
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" | ||||
@@ -1846,6 +1970,14 @@ has@^1.0.3: | |||||
dependencies: | dependencies: | ||||
function-bind "^1.1.1" | 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: | human-signals@^2.1.0: | ||||
version "2.1.0" | version "2.1.0" | ||||
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" | resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" | ||||
@@ -2222,7 +2354,7 @@ lru-cache@^6.0.0: | |||||
dependencies: | dependencies: | ||||
yallist "^4.0.0" | yallist "^4.0.0" | ||||
make-dir@^3.0.0: | |||||
make-dir@^3.0.0, make-dir@^3.1.0: | |||||
version "3.1.0" | version "3.1.0" | ||||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" | resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" | ||||
integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== | 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" | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" | ||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== | 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: | ms@2.0.0: | ||||
version "2.0.0" | version "2.0.0" | ||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" | 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" | resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" | ||||
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= | 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: | node-releases@^2.0.3: | ||||
version "2.0.4" | version "2.0.4" | ||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.4.tgz#f38252370c43854dc48aa431c766c6c398f40476" | resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.4.tgz#f38252370c43854dc48aa431c766c6c398f40476" | ||||
integrity sha512-gbMzqQtTtDz/00jQzZ21PQzdI9PyLYqUSvD0p3naOhX4odFji0ZxYdnVwPTxmSwkmxhcFImpozceidSG+AgoPQ== | 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: | npm-run-path@^4.0.1: | ||||
version "4.0.1" | version "4.0.1" | ||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" | 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: | dependencies: | ||||
path-key "^3.0.0" | 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: | nth-check@^2.0.1: | ||||
version "2.0.1" | version "2.0.1" | ||||
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.0.1.tgz#2efe162f5c3da06a28959fbd3db75dbeea9f0fc2" | 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" | resolve.exports "^1.1.0" | ||||
yargs "^17.2.1" | 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: | process-warning@^1.0.0: | ||||
version "1.0.0" | version "1.0.0" | ||||
resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-1.0.0.tgz#980a0b25dc38cd6034181be4b7726d89066b4616" | 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" | resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" | ||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== | 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" | version "3.6.0" | ||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" | ||||
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== | integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== | ||||
@@ -2825,7 +3013,7 @@ side-channel@^1.0.4: | |||||
get-intrinsic "^1.0.2" | get-intrinsic "^1.0.2" | ||||
object-inspect "^1.9.0" | 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" | version "3.0.7" | ||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" | ||||
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== | 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" | resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-4.0.4.tgz#42d01ab0b34660ea8a018da8f56a3309bb8b2a5b" | ||||
integrity sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ== | 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" | version "4.2.3" | ||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" | resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" | ||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== | 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" | resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" | ||||
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== | 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: | text-table@^0.2.0: | ||||
version "0.2.0" | version "0.2.0" | ||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" | 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: | dependencies: | ||||
is-number "^7.0.0" | 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: | tsconfig-paths@^3.14.1: | ||||
version "3.14.1" | version "3.14.1" | ||||
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" | resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" | ||||
@@ -3131,6 +3336,19 @@ wcwidth@^1.0.1: | |||||
dependencies: | dependencies: | ||||
defaults "^1.0.3" | 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: | which-boxed-primitive@^1.0.2: | ||||
version "1.0.2" | version "1.0.2" | ||||
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" | 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: | dependencies: | ||||
isexe "^2.0.0" | 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: | word-wrap@^1.2.3: | ||||
version "1.2.3" | version "1.2.3" | ||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" | resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" | ||||