diff --git a/milk/.env.example b/milk/.env.example index 307c5c9..e6582fa 100644 --- a/milk/.env.example +++ b/milk/.env.example @@ -35,4 +35,5 @@ KAFKA_HOST= # # IMPORTANT: To configure the server port for Docker Swarm, PLEASE LOOK INTO THE DOCKER COMPOSE FILE! # ---------------------- -# SERVER_PORT=7732 \ No newline at end of file +# SERVER_PORT=7732 +# TRUST_PROXY=true \ No newline at end of file diff --git a/milk/docs/[beemo-devs]/kafka.md b/milk/docs/[beemo-devs]/kafka.md new file mode 100644 index 0000000..37b9c9b --- /dev/null +++ b/milk/docs/[beemo-devs]/kafka.md @@ -0,0 +1,155 @@ +# Developer Documentations for Kafka client + +This documentation is primarily for developers of Beemo. The Kafka client isn't something +accessible to third-parties, therefore, more than likely, this is of no-use for third-parties. + +### Key Points +- [`Client Specifications`](#client-specifications) talks about the different parts of the client. + - [`overview`](#overview) summarizes some key points of the client. + - [`keys`](#keys) + - [`create-raid`](#create-raid) used to create a raid. + - [`batch-insert-raid-users`](#batch-insert-raid-users) used to insert one or more bots detected; creates the Raid if it doesn't exist. + - [`conclude-raid`](#conclude-raid) used to conclude an existing raid; if no date provided, uses current time. + - [`schemas`](#schemas) + - [`RaidManagementData`](#raid-management-data) is the primary type transported between clients. + - [`RaidManagementRequest`](#raid-management-request) is used by a requesting client to add more raid users, + start a raid or conclude a raid. Primarily created clients such as Tea. + - [`RaidManagementResponse`](#raid-management-response) is used by a responding client after processing a request. + This is primarily used by clients such as Milk. + - [`RaidManagementUser`](#raid-management-user) is used to hold information about a bot detected. + +## Client Specifications + +In this section, the different specifications of the Kafka client will be discussed and understood to +provide some understanding over how the Kafka client of Milk processes requests. + +### Overview +- Topic: `raid-management` +- Keys: + - `batch-insert-raid-users` +- Transport Type: + - [`RaidManagementData`](#raid-management-data) + +## Keys + +### Batch Insert Raid Users + +```yaml +key: batch-insert-raid-users +``` + +This is a specific key, or endpoint, in the Kafka client where clients can insert +bots detected, start a raid or conclude a raid. It is expected that this creates a new raid +when the `raidId` provided does not exist already. In addition, if the raid hasn't been concluded +but a `concludedAt` property is provided then it will conclude the raid, if the raid has been +concluded before, but a newer date has been provided then it will conclude the raid. + +This endpoint expects to receive a [`RaidManagementData`](#raid-management-data) with the `request` property +following the [`RaidManagementRequest`](#raid-management-request) schema. + +After processing the request, this endpoint should respond with a similar [`RaidManagementData`](#raid-management-data) but +with the `response` property following the [`RaidManagementResponse`](#raid-management-response) schema. + +### Conclude Raid + +```yaml +key: conclude-raid +``` + +This is a specific key, or endpoint, in the Kafka client where clients can declare an existing raid as concluded. +It is not needed for the `concludedAt` property to be provided as it will use the current time if not provided. +Although you cannot modify the `concludedAt` of an existing raid, if a raid is already concluded then it will skip. + +This endpoint expects to receive a [`RaidManagementData`](#raid-management-data) with the `request` property +following the [`RaidManagementRequest`](#raid-management-request) schema. Unlike [`batch-insert-raid-users`](#batch-insert-raid-users), +this doesn't expect the `users` property to not be empty, even `concludedAt` can be of a zero value or even +null as long as the `raidId` and `guildIdString` are not null. + +After processing the request, this endpoint should respond with a similar [`RaidManagementData`](#raid-management-data) but +with the `response` property following the [`RaidManagementResponse`](#raid-management-response) schema. + +### Create Raid + +```yaml +key: create-raid +``` + +This is a specific key, or endpoint, in the Kafka client where clients can create a new raid in the database. This should be +done at the start before the users are added, and should be awaited otherwise it will lead to a foreign key issue. + +This endpoint expects to receive a [`RaidManagementData`](#raid-management-data) with the `request` property +following the [`RaidManagementRequest`](#raid-management-request) schema. Unlike [`batch-insert-raid-users`](#batch-insert-raid-users), +this doesn't expect the `users` property to not be empty as long as the `raidId` and `guildIdString` are not null. + +After processing the request, this endpoint should respond with a similar [`RaidManagementData`](#raid-management-data) but +with the `response` property following the [`RaidManagementResponse`](#raid-management-response) schema. + +## Schemas + +### Raid Management Data + +```json +{ + "request": "nullable(RaidManagementRequest)", + "response": "nullable(RaidManagementResponse)" +} +``` + +Used by the clients to transport either a request or a response without the need to perform additional identification. + +- `request` is a nullable property containing the request details, used by the requesting client. This should be +guaranteed from a request, otherwise there is a bug with that client. +- `response` is a nullable property containing the response details, used by the responding client. This should be +guaranteed from a response of a client, otherwise there is a bug with that client. + + +### Raid Management Request +```json +{ + "raidId": "string", + "guildIdString": "string", + "users": "array(RaidManagementUser)", + "concludedAt": "nullable(date as string)" +} +``` + +Used by a requesting client to start a raid, insert bots detected or conclude a raid. + +- `raidId` refers to the internal raid id of the raid. Clients shouldn't use the external raid id as that is created +and used only by Milk itself. +- `guildIdString` refers to the id of the guild that this raid belonged to. It must be of `string` type due to +the nature of JavaScript not inherently supporting `int64` or `long` type. +- `users` refers to the bots detected in the raid, this can be an empty array when simply concluding a raid. +- `concludedAt` refers to the date when the raid should be declared as concluded. + +### Raid Management Response +```json +{ + "publicId": "nullable(string)", + "acknowledged": true +} +``` + +Used by a responding client to notify that the request was processed, and a publicly accessible id is now +available to be shared in the log channels. + +- `externalId` refers to the publicly accessible id that can be used in `/raid/:id` + +### Raid Management User +```json +{ + "idString": "string", + "name": "string", + "avatarHash": "nullable(string)", + "createdAt": "date as string", + "joinedAt": "date as string" +} +``` + +Contains information about a bot that was detected in a raid. + +- `idString` refers to the id of the bot's account. +- `name` refers to the name of the bot during detection. +- `avatarHash` refers to the hash of the bot's avatar during detection. +- `createdAt` refers to the creation time of the bot. +- `joinedAt` refers to when the bot joined the server. \ No newline at end of file diff --git a/milk/docs/json_api.md b/milk/docs/json_api.md new file mode 100644 index 0000000..7c1b70e --- /dev/null +++ b/milk/docs/json_api.md @@ -0,0 +1,201 @@ +# Documentations for Third-Party Developers + +This documentation primarily discusses the JSON API (Application Interface) available to everyone. + +## Limitations of Text API + +```text +logs.beemo.gg/raid/:id +``` + +Following some recent testings, we've discovered that raid logs reaching over 500,000 users took **tens of megabytes** +to load, and that isn't ideal for the general users and for our systems. As such, after careful consideration, we've +decided to limit the Text API (`/raid/:id`) to display, at maximum, 2,000 users, which is about as much as we expect +the public to skim at maximum without using a tool to aggregate the data. + +For people who aggregates the data using specialized tools, we have a JSON API (`/raid/:id.json`) that is paginated +available to use. You can find details about it in the following: +1. [`OpenAPI Schema`](openapi.yaml) +2. [`JSON API`](json_api.md) + +## JSON API + +```text +logs.beemo.gg/raid/:id.json +``` + +In this section, the specifications of the JSON API will be discussed and understood to provide clarity and understanding +over how one can use this to collect data about their raid. + +> **Warning** +> +> Please note that this documentation is written for people who have some understanding of general +> data types and JSON as this is intended for people who are usually developing their own in-house tools. + +### Specifications + +Our JSON API is different from the Text API as the data here is chunked into different pages by 200 users each page, +and contains more detailed information of users, such as their avatar hash, when the account was created. + +### Date Format + +Dates are formatted under the [`ISO 8601`](https://en.wikipedia.org/wiki/ISO_8601) specification[^1], which are as follows: +```text +YYYY-MM-DDTHH:mm:ss.sssZ +``` + +[^1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString + +### Response Schema +```json +{ + "raid": { + "startedAt": "date", + "concludedAt": "nullable(date)", + "guild": "int64 as string", + "size": "int32" + }, + "users": { + "next": "nullable(date)", + "size": "int16", + "data": "array of User" + } +} +``` + +### User Schema +```json +{ + "id": "int64 as string", + "name": "string", + "joinedAt": "date", + "createdAt": "date", + "avatarHash": "nullable(string)" +} +``` + +### Query Parameters +- `cursor`: an ISO-8601 date that indicates what next set of data to get. + - **description**: an ISO-8601 date that indicates what next set of data to get. + - **type**: ISO-8601 date or `null`. + - **example**: `?cursor=2023-11-02T01:02:36.978Z` + +### Querying data + +To query the initial data, one needs to send a request to `logs.beemo.gg/raid/:id.json` where `:id` refers to the +raid identifier provided by Beemo, it should look gibberish and random, such as: `Ht2Erx76nj13`. In this example, we'll +use `Ht2Erx76nj13` as our `:id`. + +> **Note** +> +> `Ht2Erx76nj13` is a sample raid from our tests. It may or may not exist on the actual `logs.beemo.gg` domain as +> of writing, but it could exist at some point. We recommend using your own raid identifier to follow along with +> our example. + +Depending on your tooling, this may be different, but in this example, we'll be using `curl` in our command-line +to demonstrate. +```shell +curl http://logs.beemo.gg/raid/Ht2Erx76nj13.json +``` + +Running the above command gives us a very long JSON response, which follows our [schema](#response-schema) containing +information about the raid and the users. +```json +{ + "raid": { + "startedAt": "2023-11-02T01:25:36.970Z", + "concludedAt": null, + "guild": "697474023914733575", + "size": 5000 + }, + "users": { + "next": "2023-11-02T01:02:36.978Z", + "size": 200, + "data": [ + { + "id": "972441010812461952", + "name": "9hsr6JA5jk7vVA35", + "joinedAt": "2023-11-02T01:01:36.975Z", + "createdAt": "2023-11-14T01:25:36.975Z", + "avatarHash": null + }, + { + "id": "495325603979310784", + "name": "TW871dd7YTb7sZv9", + "joinedAt": "2023-11-02T01:01:36.975Z", + "createdAt": "2023-11-03T01:25:36.975Z", + "avatarHash": null + }, + ..., + { + "id": "732275137613676288", + "name": "EbWp0ORBYH33BOX5", + "joinedAt": "2023-11-02T01:02:36.978Z", + "createdAt": "2023-11-21T01:25:36.978Z", + "avatarHash": null + } + ] + } +} +``` + +From the above output, we can see that the `next` property is filled, which means, we can use that to possibly query +the next set of data, if there is any. This `next` property also happens to be the same as the `joinedAt` time of the +**last user in the array**, in this case, the user with the id of "732275137613676288" and the name of "EbWp0ORBYH33BOX5". + +To query the next set of information, all we need to do is add `?cursor=` to the link. In our +example, our `` is `2023-11-02T01:02:36.978Z`, which means, we have to add `?cursor=2023-11-02T01:02:36.978Z` to our link. + +```shell +curl http://logs.beemo.gg/raid/Ht2Erx76nj13.json?cursor=2023-11-02T01:02:36.978Z +``` + +Running that command on our command-line gives us a similar output to the above, but instead, we get a different set +of users and a different `next` property value that directly links with the **last user in the array**'s `joinedAt`. + +```json +{ + "raid": { + "startedAt": "2023-11-02T01:25:36.970Z", + "concludedAt": null, + "guild": "697474023914733575", + "size": 5000 + }, + "users": { + "next": "2023-11-02T01:03:36.982Z", + "size": 200, + "data": [ + { + "id": "819796273096448256", + "name": "676kq1AUwXh8Ygwf", + "joinedAt": "2023-11-02T01:02:36.979Z", + "createdAt": "2023-11-09T01:25:36.979Z", + "avatarHash": null + }, + { + "id": "679522196993485440", + "name": "93j0HK2MvEud9n1Y", + "joinedAt": "2023-11-02T01:02:36.979Z", + "createdAt": "2023-11-26T01:25:36.979Z", + "avatarHash": null + }, + ..., + { + "id": "151281220654798656", + "name": "W04Qy8a4p1bZxSMu", + "joinedAt": "2023-11-02T01:03:36.982Z", + "createdAt": "2023-11-25T01:25:36.982Z", + "avatarHash": null + } + ] + } +} +``` + +And similar to the previous one, you can then request the next set of data by replacing the value of `cursor` on your +link to the value of `next` and keep repeating until you get all the data that you want. + +## Have more questions? + +If you have more questions, feel free to come over at our [Discord Server](https://beemo.gg/discord) and ask over there, +we'll happily answer to our best capacity! \ No newline at end of file diff --git a/milk/docs/openapi.yaml b/milk/docs/openapi.yaml new file mode 100644 index 0000000..e39f9b8 --- /dev/null +++ b/milk/docs/openapi.yaml @@ -0,0 +1,128 @@ +openapi: 3.0.3 +info: + title: Milk by Beemo + description: |- + The official raid log manager of Beemo. It is used to store raid logs to the database, and also to allow people to view raid logs through a text or JSON format. + + In this documentation, we will be primarily describing the JSON output of the service as this is primarily why other developers would visit this. + termsOfService: https://beemo.gg/terms + contact: + email: hello@beemo.gg + version: 1.0.0 +servers: + - url: https://logs.beemo.gg +tags: + - name: raid + description: Everything public about raids caught by Beemo. +paths: + /raid/{id}.json: + get: + tags: + - raid + summary: Gets information about a raid in JSON format. + description: Returns more detailed information about a raid in JSON format. + operationId: getRaidById + parameters: + - name: id + in: path + description: ID of the raid. + required: true + schema: + type: string + - name: cursor + in: query + description: The last `joined_at` date to query the next page, found in the `next` property. + required: false + schema: + type: string + format: date + example: '2023-10-30T16:10:57.697Z' + responses: + '200': + description: Raid found + content: + application/json: + schema: + $ref: '#/components/schemas/CursoredRaid' + headers: + 'X-Cache': + description: 'Indicates whether the response was cached or not.' + schema: + type: string + example: + - 'HIT' + - 'MISS' + 'X-Cache-Expires': + description: 'Indicates the timestamp when the cache will expire.' + schema: + type: integer + example: '1698767766556' + '404': + description: Raid not found + '400': + description: Invalid date provided for `cursor` query string. +components: + schemas: + CursoredRaid: + type: object + properties: + raid: + $ref: '#/components/schemas/Raid' + users: + type: object + properties: + next: + type: string + format: date + example: '2023-10-30T15:29:57.697Z' + nullable: true + size: + type: integer + format: int32 + example: 200 + data: + type: array + items: + $ref: '#/components/schemas/RaidUser' + Raid: + type: object + properties: + size: + type: integer + format: int32 + example: 32 + startedAt: + type: string + format: date + example: '2023-10-30T15:29:57.697Z' + nullable: true + concludedAt: + type: string + format: date + example: '2023-10-30T16:10:57.697Z' + nullable: true + guild: + type: string + format: int64 + example: '697474023914733575' + RaidUser: + type: object + properties: + id: + type: string + format: int64 + example: '584322030934032393' + name: + type: string + example: 'Shindou Mihou' + avatarHash: + type: string + nullable: true + createdAt: + type: string + format: datetime + example: '2023-10-30T16:10:57.697Z' + joinedAt: + type: string + format: datetime + example: '2023-10-30T16:10:57.697Z' \ No newline at end of file diff --git a/milk/package.json b/milk/package.json index 929c7a6..5bf8dd4 100644 --- a/milk/package.json +++ b/milk/package.json @@ -8,21 +8,26 @@ "type": "module", "dependencies": { "@beemobot/common": "^1.0.0", - "@prisma/client": "4.8.1", + "@prisma/client": "^5.5.2", "@sentry/node": "^7.29.0", "dotenv": "^16.0.3", "fastify": "^4.11.0", + "fastify-plugin": "^4.5.1", "node-cache": "^5.1.2", "typescript": "^4.9.4" }, "scripts": { "build": "rimraf dist && tsc", "serve": "node --enable-source-maps dist/index.js", + "dev": "rimraf dist && tsc && node --enable-source-maps dist/index.js", "clean": "rimraf dist" }, "devDependencies": { "@types/node": "^18.11.18", - "prisma": "^4.8.1", + "prisma": "^5.5.2", "rimraf": "^5.0.1" + }, + "prisma": { + "seed": "node ./prisma/seed.js" } } diff --git a/milk/prisma/.gitignore b/milk/prisma/.gitignore new file mode 100644 index 0000000..7eafe31 --- /dev/null +++ b/milk/prisma/.gitignore @@ -0,0 +1,2 @@ +# Temporarily ignored until a better seeding method is present. +seed.js \ No newline at end of file diff --git a/milk/prisma/schema.prisma b/milk/prisma/schema.prisma index 5b623dc..0385d07 100644 --- a/milk/prisma/schema.prisma +++ b/milk/prisma/schema.prisma @@ -8,17 +8,23 @@ datasource db { } model RaidUser { - internal_raid_id String @db.Uuid - user_id BigInt @unique - name String - avatar_hash String? - created_at DateTime @db.Timestamptz - joined_at DateTime @db.Timestamptz + raid Raid? @relation(fields: [raid_id], references: [id]) + raid_id String + id BigInt + name String + avatar_hash String? + created_at DateTime @db.Timestamptz + joined_at DateTime @db.Timestamptz + + @@id([raid_id, id]) + @@index([joined_at]) } model Raid { - internal_id String @unique @db.Uuid - external_id String @unique - guild_id BigInt - concluded_at DateTime? @db.Timestamptz -} \ No newline at end of file + id String @unique + public_id String @unique + guild_id BigInt + created_at DateTime @default(now()) @db.Timestamptz() + concluded_at DateTime? @db.Timestamptz + users RaidUser[] +} diff --git a/milk/src/cache/antispamLogsCache.ts b/milk/src/cache/antispamLogsCache.ts deleted file mode 100644 index 21852f8..0000000 --- a/milk/src/cache/antispamLogsCache.ts +++ /dev/null @@ -1,5 +0,0 @@ -import NodeCache from "node-cache"; - -export const AntispamLogsCache = new NodeCache({ - stdTTL: 10 * 1000 * 60 -}) \ No newline at end of file diff --git a/milk/src/connections/fastify.ts b/milk/src/connections/fastify.ts index 5416cd9..ef0cb19 100644 --- a/milk/src/connections/fastify.ts +++ b/milk/src/connections/fastify.ts @@ -1,45 +1,39 @@ import {Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe -import {TAG} from "../index.js"; -import Fastify, {FastifyInstance} from "fastify"; -import {GetAntispam} from "../routes/GetAntispam.js"; -import {LogHook} from "../hooks/LogHook.js"; -import {ErrorAndNotFoundHook} from "../hooks/ErrorAndNotFoundHook.js"; -import {DefaultRoute} from "../routes/DefaultRoute.js"; -import {Attachable} from "../types/fastify.js"; +import Fastify from "fastify"; +import GetAntispam from "../routes/get_raid.js"; +import LogHook from "../hooks/log_hook.js"; +import ErrorHook from "../hooks/error_hook.js"; +import DefaultRoute from "../routes/default_route.js"; +import {logError, logIssue} from "./sentry.js"; +import {TAG} from "../constants/logging.js"; -export const server: FastifyInstance = Fastify.default({ ignoreTrailingSlash: true, ignoreDuplicateSlashes: true }) -export const attachables: Attachable[] = [ - ErrorAndNotFoundHook, - LogHook, - GetAntispam, - DefaultRoute -] +const server = Fastify.default({ + ignoreTrailingSlash: true, + ignoreDuplicateSlashes: true, + trustProxy: (process.env.TRUST_PROXY ?? 'false').toLowerCase() === 'true', + disableRequestLogging: true +}) -async function init() { - if (!process.env.SERVER_PORT || Number.isNaN(process.env.SERVER_PORT)) { - Logger.error(TAG, 'Server Port is not configured, discarding request to start.') - process.exit() - return - } - - for (const attachable of attachables) { - await attachable.attach(server) - } +export async function initializeFastify() { + try { + if (!process.env.SERVER_PORT || Number.isNaN(process.env.SERVER_PORT)) { + logIssue('You need to configure a server port for the service to work.') + return + } - const port = Number.parseInt(process.env.SERVER_PORT) - const link = 'http://localhost:' + port + for (const attachable of [ErrorHook, LogHook, GetAntispam, DefaultRoute]) { + attachable(server) + } - await server.listen({ - port: port, - host: '0.0.0.0' - }) + const port = Number.parseInt(process.env.SERVER_PORT) + await server.listen({ + port: port, + host: '0.0.0.0' + }) - Logger.info(TAG, 'Fastify Server is now running ' + JSON.stringify({ - port: port, - antispam: link + '/antispam/', - messages: link + '/messages/' - })) -} - -export const Fastified = { init: init } \ No newline at end of file + Logger.info(TAG, `Milk is now serving logs under port ${port}.`) + } catch (ex) { + logError('An issue occurred while trying to start Fastify.', ex) + } +} \ No newline at end of file diff --git a/milk/src/connections/kafka.ts b/milk/src/connections/kafka.ts index 2bc5b62..7d7bd50 100644 --- a/milk/src/connections/kafka.ts +++ b/milk/src/connections/kafka.ts @@ -1,23 +1,24 @@ import {KafkaConnection, Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe -import {TAG} from "../index.js"; -import {KafkaClients} from "../kafka/clients.js"; +import {initKafkaClients} from "../kafka/clients.js"; +import {logIssue} from "./sentry.js"; +import {TAG} from "../constants/logging.js"; export let kafka: KafkaConnection -async function init() { +export async function initializeKafka() { process.env.KAFKAJS_NO_PARTITIONER_WARNING = "1" + if (!process.env.KAFKA_HOST) { - Logger.error(TAG, 'Kafka is not configured, discarding request to start.') - process.exit() + logIssue('Kafka is needed to start this service. If you need to run this for read-only, ' + + 'please properly configure that on the configuration.') return } - Logger.info(TAG, "Attempting to start Kafka " + JSON.stringify({ host: process.env.KAFKA_HOST })) - kafka = new KafkaConnection(process.env.KAFKA_HOST, "milk", "milk", "-5") - await kafka.start() + Logger.info(TAG, "Attempting to connect to Kafka " + JSON.stringify({ host: process.env.KAFKA_HOST })) - KafkaClients.init(kafka) -} + kafka = new KafkaConnection(process.env.KAFKA_HOST, "milk", "milk", "-5") + initKafkaClients(kafka) -export const Koffaka = { init: init } \ No newline at end of file + await kafka.start() +} \ No newline at end of file diff --git a/milk/src/connections/prisma.ts b/milk/src/connections/prisma.ts index 7c561e0..e8187e5 100644 --- a/milk/src/connections/prisma.ts +++ b/milk/src/connections/prisma.ts @@ -1,20 +1,19 @@ -import {Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe -import {prisma, TAG} from "../index.js"; -async function init() { +import {logError, logIssue} from "./sentry.js"; +import {PrismaClient} from "@prisma/client"; + +export let prisma = new PrismaClient() + +export async function initializePrisma() { try { if (process.env.DATABASE_URL == null) { - Logger.error(TAG, 'Prisma is not configured, discarding request to start.') - process.exit() + logIssue('No database URI has been found on the configuration. Please configure it as the service cannot run without it.') return } await prisma.$connect() } catch (ex) { - Logger.error(TAG, 'Failed to connect to Prisma, closing startup.') - console.error(ex) + logError('Failed to connect to the database, closing service.', ex) process.exit() } -} - -export const Prismae = { init: init } \ No newline at end of file +} \ No newline at end of file diff --git a/milk/src/connections/sentry.ts b/milk/src/connections/sentry.ts index a12faea..31e9e4a 100644 --- a/milk/src/connections/sentry.ts +++ b/milk/src/connections/sentry.ts @@ -1,15 +1,25 @@ import * as Sentry from '@sentry/node' import {Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe -import {TAG} from "../index.js"; -function init() { + +import {TAG} from "../constants/logging.js"; +export function initializeSentry() { if (process.env.SENTRY_DSN == null) { - Logger.error(TAG, 'Sentry is not configured, discarding request to start.') - process.exit() + Logger.warn(TAG, 'Sentry is not configured, we recommend configuring Sentry to catch issues properly.') return } - Sentry.init({ dsn: process.env.SENTRY_DSN, tracesSampleRate: 1.0, }) + Sentry.init({dsn: process.env.SENTRY_DSN, tracesSampleRate: 1.0,}) } -export const Sentryboo = { init: init } \ No newline at end of file +export function logIssue(message: string) { + const error = Error(message) + + Sentry.captureException(error) + Logger.error(TAG, error.message) +} + +export function logError(message: string, ex: any) { + Sentry.captureException(ex) + Logger.error(TAG, message, ex) +} diff --git a/milk/src/constants/errors.ts b/milk/src/constants/errors.ts new file mode 100644 index 0000000..029350a --- /dev/null +++ b/milk/src/constants/errors.ts @@ -0,0 +1,2 @@ +const createError = (message: string) => JSON.stringify({ error: message }) +export const INVALID_CURSOR_QUERY_STRING = createError('Provided `cursor` query string is not a valid date.') \ No newline at end of file diff --git a/milk/src/constants/links.ts b/milk/src/constants/links.ts new file mode 100644 index 0000000..8827c39 --- /dev/null +++ b/milk/src/constants/links.ts @@ -0,0 +1 @@ +export const JSON_API_DOCUMENTATIONS = "https://github.com/beemobot/cafe/blob/main/milk/docs/json_api.md" \ No newline at end of file diff --git a/milk/src/constants/logging.ts b/milk/src/constants/logging.ts new file mode 100644 index 0000000..6485508 --- /dev/null +++ b/milk/src/constants/logging.ts @@ -0,0 +1 @@ +export const TAG = "Milk" \ No newline at end of file diff --git a/milk/src/constants/pagination.ts b/milk/src/constants/pagination.ts new file mode 100644 index 0000000..3035242 --- /dev/null +++ b/milk/src/constants/pagination.ts @@ -0,0 +1 @@ +export const PAGINATION_LIMIT = 200 \ No newline at end of file diff --git a/milk/src/constants/raid_management_kafka.ts b/milk/src/constants/raid_management_kafka.ts new file mode 100644 index 0000000..d3035be --- /dev/null +++ b/milk/src/constants/raid_management_kafka.ts @@ -0,0 +1,4 @@ +export const RAID_MANAGEMENT_CLIENT_TOPIC = "raid-management" +export const RAID_MANAGEMENT_CREATE_RAID = "create-raid" +export const RAID_MANAGEMENT_BATCH_INSERT_KEY = "batch-insert-raid-users" +export const RAID_MANAGEMENT_CONCLUDE_RAID = "conclude-raid" \ No newline at end of file diff --git a/milk/src/constants/time.ts b/milk/src/constants/time.ts new file mode 100644 index 0000000..a7139f3 --- /dev/null +++ b/milk/src/constants/time.ts @@ -0,0 +1 @@ +export const TEN_MINUTES = 60 * 10 diff --git a/milk/src/database/raid.ts b/milk/src/database/raid.ts new file mode 100644 index 0000000..fb06e8d --- /dev/null +++ b/milk/src/database/raid.ts @@ -0,0 +1,53 @@ +import {prisma} from "../connections/prisma.js"; +import {Raid} from "@prisma/client"; +import {randomString} from "../utils/string.js"; + +/** + * Given an {@link publicId}, gets information about a given {@link Raid} with the {@link RaidUser} + * relation included. + * + * @param publicId the external id of the raid. + * @return {@link Raid} or null if it doesn't exist. + */ +export const getRaidByPublicId = async (publicId: string) => { + return (await prisma.raid.findUnique({where: {public_id: publicId}})); +} + +/** + * Given an {@link id}, gets information about a given {@link Raid}. + * @param id the external id of the raid. + * @return {@link Raid} or null if it doesn't exist. + */ +export const getRaidByInternalId = async (id: string): Promise => + (await prisma.raid.findUnique({where: {id}})) + +/** + * Concludes a {@link Raid} in the database. + * + * @param publicId the public id of the raid. + * @param id the internal id of the raid. + * @param concludedAt the time when the raid was concluded. + */ +export const concludeRaid = async (publicId: string, id: string, concludedAt: Date | null) => + (await prisma.raid.update({ + where: { id, public_id: publicId }, + data: { concluded_at: concludedAt } + })) + +/** + * Creates a new {@link Raid}. + * + * @param id the internal id of the raid. + * @param guildId the id of the guild being raided. + * @param concludedAt the conclusion time of the raid, if provided. + */ +export const createRaid = async (id: string, guildId: string, concludedAt: Date | null) => ( + await prisma.raid.create({ + data: { + id: id, + public_id: randomString(12), + guild_id: BigInt(guildId), + concluded_at: concludedAt + } + }) +) \ No newline at end of file diff --git a/milk/src/database/raid_users.ts b/milk/src/database/raid_users.ts new file mode 100644 index 0000000..ca8775e --- /dev/null +++ b/milk/src/database/raid_users.ts @@ -0,0 +1,61 @@ +import {prisma} from "../connections/prisma.js"; +import {PublicRaidUser} from "../types/raid.js"; +import {Raid, RaidUser} from "@prisma/client"; + +const transformToPublicRaidUser = (user: RaidUser): PublicRaidUser => { + return { + id: user.id.toString(), + name: user.name, + joinedAt: user.joined_at, + createdAt: user.created_at, + avatarHash: user.avatar_hash + } satisfies PublicRaidUser +} + +/** + * Gets the users involved in a {@link Raid} as a {@link PublicRaidUser} type which does not include the + * `internal_raid_id`. + * + * @param raid the raid to get the data from + * @return the users involved in a {@link Raid}. + */ +export const getPublicRaidUsers = (raid: { users: RaidUser[] } & Raid): PublicRaidUser[] => raid.users + .map(transformToPublicRaidUser) + +/** + * Paginates over the {@link RaidUser} associated in a given {@link Raid}. + * + * @param raidId the raid id + * @param limit the maximum users to return + * @param cursor the cursor (joined_at) to use. + * @return all the users associated in a {@link Raid} given a cursor. + */ +export const paginateUsers = async (raidId: string, limit: number, cursor: Date | null): Promise<{ users: PublicRaidUser[], count: number }> => { + const results = await prisma.$transaction([ + prisma.raidUser.findMany({ + where: { + raid_id: raidId, + joined_at: cursor == null ? undefined : { + gt: cursor + } + }, + take: limit, + orderBy: { + joined_at: 'asc' + } + }), + prisma.raidUser.count({ + where: { + raid_id: raidId + } + }) + ]) + return { users: results[0].map(transformToPublicRaidUser), count: results[1] } +} + +/** + * Inserts many {@link RaidUser} to the database. + * @param users the users to insert into the database. + */ +export const insertRaidUsers = async (users: RaidUser[]) => + (await prisma.raidUser.createMany({data: users, skipDuplicates: true})) \ No newline at end of file diff --git a/milk/src/fastify/serve_cached.ts b/milk/src/fastify/serve_cached.ts new file mode 100644 index 0000000..83bd66e --- /dev/null +++ b/milk/src/fastify/serve_cached.ts @@ -0,0 +1,33 @@ +import NodeCache from "node-cache"; +import {FastifyReply} from "fastify"; +import {TEN_MINUTES} from "../constants/time.js"; + +const cache = new NodeCache({ stdTTL: TEN_MINUTES }) +export type CacheResult = { result: string | null, shouldCache: boolean } + +const discordCacheResult = { result: null, shouldCache: false } +export const useCacheWhenPossible = async ( + reply: FastifyReply, + key: string, + contentType: string, + computation: (discard: CacheResult) => Promise +): Promise => { + const cachedResult = cache.get(key) + if (cachedResult != null) { + return reply + .header('X-Cache', 'HIT') + .header('X-Cache-Expires', cache.getTtl(key)) + .header('Content-Type', contentType) + .send(cachedResult) + } + + const { result, shouldCache } = await computation(discordCacheResult) + if (shouldCache) { + cache.set(key, result) + } + return reply + .status(200) + .header('X-Cache', 'MISS') + .header('Content-Type', contentType) + .send(result) +} \ No newline at end of file diff --git a/milk/src/hooks/ErrorAndNotFoundHook.ts b/milk/src/hooks/ErrorAndNotFoundHook.ts deleted file mode 100644 index 493ba1e..0000000 --- a/milk/src/hooks/ErrorAndNotFoundHook.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {FastifyInstance} from "fastify"; -import * as Sentry from "@sentry/node"; - -const attach = (server: FastifyInstance) => server - .setNotFoundHandler((_, reply) => { reply.code(404).send('404 Not Found') }) - .setErrorHandler((error, request, reply) => { - if (reply.statusCode === 429) { - reply.send('You are sending too many requests, slow down!') - return - } - - Sentry.captureException(error) - reply.code(500).send('An error occurred on the server-side, the hive has been notified.') - }) - -export const ErrorAndNotFoundHook = { attach: attach } \ No newline at end of file diff --git a/milk/src/hooks/LogHook.ts b/milk/src/hooks/LogHook.ts deleted file mode 100644 index 2852fa7..0000000 --- a/milk/src/hooks/LogHook.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {Logger} from "@beemobot/common"; -// ^ This needs to be updated; Probably @beemobot/cafe -import {TAG} from "../index.js"; -import {FastifyInstance} from "fastify"; - -const attach = (server: FastifyInstance) => server.addHook( - 'preHandler', - async (request) => Logger.info(TAG, 'Request ' + JSON.stringify({ method: request.method, url: request.url, ip: request.ip })) -) -export const LogHook = { attach: attach } \ No newline at end of file diff --git a/milk/src/hooks/error_hook.ts b/milk/src/hooks/error_hook.ts new file mode 100644 index 0000000..c4f739d --- /dev/null +++ b/milk/src/hooks/error_hook.ts @@ -0,0 +1,18 @@ +import {FastifyInstance} from "fastify"; +import * as Sentry from "@sentry/node"; + +export default async (fastify: FastifyInstance) => { + fastify + .setNotFoundHandler((_, reply) => { reply.code(404).send('404 Not Found') }) + .setErrorHandler((error, _, reply) => { + if (reply.statusCode === 429) { + reply.send('You are sending too many requests, slow down!') + return + } + + if (process.env.SENTRY_DSN != null) { + Sentry.captureException(error) + } + reply.code(500).send('An error occurred on the server-side, the hive has been notified.') + }) +} \ No newline at end of file diff --git a/milk/src/hooks/log_hook.ts b/milk/src/hooks/log_hook.ts new file mode 100644 index 0000000..66f01b3 --- /dev/null +++ b/milk/src/hooks/log_hook.ts @@ -0,0 +1,12 @@ +import {Logger} from "@beemobot/common"; +// ^ This needs to be updated; Probably @beemobot/cafe +import {FastifyInstance} from "fastify"; +import {TAG} from "../constants/logging.js"; + +export default async (fastify: FastifyInstance) => { + fastify.addHook( + 'preHandler', + async (request) => + Logger.info(TAG, 'Request ' + JSON.stringify({ method: request.method, url: request.url, ip: request.ip })) + ) +} \ No newline at end of file diff --git a/milk/src/index.ts b/milk/src/index.ts index 8889f15..9f6faf7 100644 --- a/milk/src/index.ts +++ b/milk/src/index.ts @@ -1,32 +1,37 @@ -import {PrismaClient} from "@prisma/client"; import dotenv from 'dotenv' -import {Sentryboo} from "./connections/sentry.js"; -import {Koffaka} from "./connections/kafka.js"; -import {Prismae} from "./connections/prisma.js"; +import {initializeSentry, logIssue} from "./connections/sentry.js"; +import {initializeKafka} from "./connections/kafka.js"; +import {initializePrisma, prisma} from "./connections/prisma.js"; import {Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe -import {Fastified} from "./connections/fastify.js"; +import {initializeFastify} from "./connections/fastify.js"; import * as Sentry from '@sentry/node' +import {TAG} from "./constants/logging.js"; dotenv.config() -export let prisma = new PrismaClient() -export const TAG = "Milk" -export const CONFIGURATION = { - WRITES_ENABLED: process.env.WRITES_ENABLED?.toLowerCase() === 'true', - READS_ENABLED: process.env.READS_ENABLED?.toLowerCase() === 'true' -} async function main() { - Sentryboo.init() - await Prismae.init() + initializeSentry() + await initializePrisma() + + const configuration = { + writesEnabled: process.env.WRITES_ENABLED?.toLowerCase() === 'true', + readsEnabled: process.env.READS_ENABLED?.toLowerCase() === 'true' + } + + Logger.info(TAG, 'Starting milk under the following conditions ' + JSON.stringify(configuration)) + + if (!configuration.readsEnabled && !configuration.writesEnabled) { + logIssue('Milk needs to be in at least read or write mode to function.') + return + } - Logger.info(TAG, 'Starting milk under the following conditions ' + JSON.stringify(CONFIGURATION)) - if (CONFIGURATION.WRITES_ENABLED) { - await Koffaka.init() + if (configuration.writesEnabled) { + await initializeKafka() } - if (CONFIGURATION.READS_ENABLED) { - await Fastified.init() + if (configuration.readsEnabled) { + await initializeFastify() } } diff --git a/milk/src/kafka/clients.ts b/milk/src/kafka/clients.ts index 7c9844f..242911e 100644 --- a/milk/src/kafka/clients.ts +++ b/milk/src/kafka/clients.ts @@ -3,8 +3,6 @@ import {KafkaConnection} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe export let raidManagementClient: RaidManagementClient -function init(connection: KafkaConnection) { +export function initKafkaClients(connection: KafkaConnection) { raidManagementClient = new RaidManagementClient(connection) -} - -export const KafkaClients = { init: init } \ No newline at end of file +} \ No newline at end of file diff --git a/milk/src/kafka/clients/raids.ts b/milk/src/kafka/clients/raids.ts index 3e1aad2..2c9dd78 100644 --- a/milk/src/kafka/clients/raids.ts +++ b/milk/src/kafka/clients/raids.ts @@ -1,101 +1,108 @@ -import {BrokerClient, KafkaConnection} from "@beemobot/common"; +import {BrokerClient, BrokerMessage, KafkaConnection, Logger} from "@beemobot/common"; // ^ This needs to be updated; Probably @beemobot/cafe -import {prisma} from "../../index.js"; -import {StringUtil} from "../../utils/string.js"; -import {retriable} from "../../utils/retriable.js"; +import {run} from "../../utils/retry.js"; +import {RaidManagementData} from "../../types/raid.js"; +import {logIssue} from "../../connections/sentry.js"; +import {TAG} from "../../constants/logging.js"; +import { + RAID_MANAGEMENT_BATCH_INSERT_KEY, + RAID_MANAGEMENT_CLIENT_TOPIC, + RAID_MANAGEMENT_CONCLUDE_RAID, RAID_MANAGEMENT_CREATE_RAID +} from "../../constants/raid_management_kafka.js"; +import {concludeRaid, createRaid, getRaidByInternalId} from "../../database/raid.js"; +import {RaidUser} from "@prisma/client"; +import {insertRaidUsers} from "../../database/raid_users.js"; -export const KEY_BATCH_INSERT_RAID_USERS = "batch-insert-raid-users" export class RaidManagementClient extends BrokerClient { constructor(conn: KafkaConnection) { - super(conn, "raid-management"); - this.on(KEY_BATCH_INSERT_RAID_USERS, async (m) => { - if (m.value == null) { - return - } + super(conn, RAID_MANAGEMENT_CLIENT_TOPIC); + this.on(RAID_MANAGEMENT_CREATE_RAID, this.onCreateRaid) + this.on(RAID_MANAGEMENT_BATCH_INSERT_KEY, this.onBatchInsertRaidUsers) + this.on(RAID_MANAGEMENT_CONCLUDE_RAID, this.onConcludeRaid) + } + + private async onCreateRaid(message: BrokerMessage) { + if (message.value == null || message.value.request == null) { + Logger.warn(TAG, `Received a message on ${RAID_MANAGEMENT_CREATE_RAID} but no request details was found.`) + return + } + + const request = message.value.request + + let raid = await getRaidByInternalId(request.raidId) + if (raid == null) { + Logger.info(TAG, `Creating raid ${request.raidId} from guild ${request.guildId}.`) + raid = await run( + 'create_raid', + async () => createRaid(request.raidId, request.guildId, null), + 1, + 12 + ) + } + + await message.respond({ response: { publicId: raid?.public_id, acknowledged: true }, request: null }) + } + + private async onConcludeRaid(message: BrokerMessage) { + if (message.value == null || message.value.request == null) { + Logger.warn(TAG, `Received a message on ${RAID_MANAGEMENT_CONCLUDE_RAID} but no request details was found.`) + return + } + + let {raidId, concludedAtMillis, guildId} = message.value.request + let conclusionDate: Date = new Date(concludedAtMillis ?? Date.now()) + + let raid = await getRaidByInternalId(raidId) + if (raid == null) { + logIssue(`Received a request to conclude a raid, but the raid is not in the database. [raid=${raidId}]`) + return + } + + if (raid.concluded_at != null) { + Logger.warn(TAG, `Received a request to conclude a raid, but the raid is already concluded. [raid=${raidId}]`) + return + } + + Logger.info(TAG, `Concluding raid ${raidId} from guild ${guildId}.`) + raid = await run( + 'conclude_raid', + async () => concludeRaid(raid!.public_id, raid!.id, conclusionDate), + 0.2, + 12 + ) + + await message.respond({ response: { publicId: raid!.public_id, acknowledged: true }, request: null }) + } + + private async onBatchInsertRaidUsers(message: BrokerMessage) { + if (message.value == null || message.value.request == null) { + Logger.warn(TAG, `Received a message on ${RAID_MANAGEMENT_BATCH_INSERT_KEY} but no request details was found.`) + return + } - const request = m.value.request - if (request == null) { - return - } + const request = message.value.request - if (request.users.length > 0) { - await retriable( - 'insert_raid_users', - async () => { - prisma.raidUser.createMany({ - data: request.users.map((user) => { - return { - internal_raid_id: request.raidId, - user_id: BigInt(user.idString), - name: user.name, - avatar_hash: user.avatarHash, - created_at: user.createdAt, - joined_at: user.joinedAt - } - }), - skipDuplicates: true - }) - }, - 2, - 25 - ) - } + if (request.users.length > 0) { + Logger.info(TAG, `Inserting ${request.users.length} users to the raid ${request.raidId}.`) + const users = request.users.map((user) => { + return { + raid_id: request.raidId, + id: BigInt(user.id), + name: user.name, + avatar_hash: user.avatarHash, + created_at: new Date(user.createdAtMillis), + joined_at: new Date(user.joinedAtMillis) + } satisfies RaidUser + }) - let raid = await prisma.raid.findUnique({where: {internal_id: request.raidId}}).then((result) => result); - if (raid == null) { - raid = await retriable( - 'create_raid', - async () => { - const uuid = StringUtil.random(12) - return prisma.raid.create({ - data: { - internal_id: request.raidId, - external_id: uuid, - guild_id: BigInt(request.guildIdString), - concluded_at: request.concluded_at - } - }); - }, - 1, - 25 - ) - } else { - if (request.concluded_at != null && (raid.concluded_at == null || raid.concluded_at !== new Date(request.concluded_at))) { - raid = await retriable( - 'conclude_raid', - async () => { - return prisma.raid.update({ - where: { external_id: raid!.external_id, internal_id: request.raidId }, - data: { concluded_at: request.concluded_at } - }) - }, - 0.2, - 25 - ) - } - } + await run( + 'insert_raid_users', + async () => insertRaidUsers(users), + 2, + 12 + ) + } - await m.respond({ response: { externalId: raid!.external_id }, request: null }) - }) + await message.respond({ response: { publicId: null, acknowledged: true }, request: null }) } -} -export type RaidManagementData = { - request: RaidManagementRequest | null, - response: RaidManagementResponse | null -} -export type RaidManagementRequest = { - raidId: string, - guildIdString: string, - users: RaidManagementUser[], - concluded_at: (Date | string) | null -} -export type RaidManagementResponse = { - externalId: string -} -export type RaidManagementUser = { - idString: string, - name: string, - avatarHash: string | null, - createdAt: Date | string, - joinedAt: Date | string } \ No newline at end of file diff --git a/milk/src/routes/DefaultRoute.ts b/milk/src/routes/DefaultRoute.ts deleted file mode 100644 index 2a8ba43..0000000 --- a/milk/src/routes/DefaultRoute.ts +++ /dev/null @@ -1,4 +0,0 @@ -import {FastifyInstance} from "fastify"; - -const attach = (server: FastifyInstance) => server.get('/', (request, reply) => reply.redirect('https://beemo.gg')) -export const DefaultRoute = { attach: attach } \ No newline at end of file diff --git a/milk/src/routes/GetAntispam.ts b/milk/src/routes/GetAntispam.ts deleted file mode 100644 index c6eae2c..0000000 --- a/milk/src/routes/GetAntispam.ts +++ /dev/null @@ -1,55 +0,0 @@ -import {prisma, TAG} from "../index.js"; -import {Logger} from "@beemobot/common"; -// ^ This needs to be updated; Probably @beemobot/cafe -import {DateUtil} from "../utils/date.js"; -import {AntispamLogsCache} from "../cache/antispamLogsCache.js"; -import {FastifyInstance} from "fastify"; - -const attach = (server: FastifyInstance) => { - server.get('/antispam', (request, reply) => reply.send('You came to the wrong spot, buddy!')) - server.get<{Params:{ id: string}}>('/antispam/:id', async (request, reply) => { - try { - const { id } = request.params - const cache = AntispamLogsCache.get(id) - - if (cache != null) { - return reply.send(cache) - } - - const raid = await prisma.raid.findUnique({ where: { external_id: id } }) - - if (raid == null) { - return reply.code(404).send('404 Not Found') - } - - const users = await prisma.raidUser.findMany({ where: { internal_raid_id: raid.internal_id } }) - - let response = 'Userbot raid detected against server ' + raid.guild_id + ' on ' + DateUtil.toDateString(users[0].joined_at); - if (users.length === 0) { - Logger.warn(TAG, "Raid " + id + " reported no users.") - response += "\nThere are no users logged for this raid, at this moment. It is likely that the raid is still being processed, please come back later!" - } else { - response += '\nRaid size: ' + users.length + ' accounts' - response += '\n' - response += '\n Joined at: ID: Username:' - response += '\n' - let userIds = ''; - for (const user of users) { - response += DateUtil.toTimeString(user.joined_at) + ' ' + user.user_id + ' ' + user.name - userIds += user.user_id - } - - response += '\n' - response += '\n Raw IDs:' - response += '\n' - response += userIds - AntispamLogsCache.set(id, response) - } - return reply.send(response) - } catch (ex) { - console.error(ex) - } - }) -} - -export const GetAntispam = { attach: attach } \ No newline at end of file diff --git a/milk/src/routes/default_route.ts b/milk/src/routes/default_route.ts new file mode 100644 index 0000000..e9cfa37 --- /dev/null +++ b/milk/src/routes/default_route.ts @@ -0,0 +1,7 @@ +import {FastifyInstance} from "fastify"; + +export default (fastify: FastifyInstance) => { + fastify.get('/', (_, reply) => { + reply.redirect('https://beemo.gg') + }) +} \ No newline at end of file diff --git a/milk/src/routes/get_raid.ts b/milk/src/routes/get_raid.ts new file mode 100644 index 0000000..9bde62a --- /dev/null +++ b/milk/src/routes/get_raid.ts @@ -0,0 +1,19 @@ +import {FastifyInstance} from "fastify"; +import {route$GetRaidAsJson} from "./get_raid/get_raid_as_json.js"; +import {route$GetRaidAsText} from "./get_raid/get_raid_as_text.js"; +import {route$GetAntispam} from "./get_raid/get_antispam.js"; + +export type RaidParameter = { + Params: { id: string } +} + +export type CursoredRaidParameter = { + Params: { id: string }, + Querystring: { cursor: string | null } +} + +export default async (fastify: FastifyInstance) => { + fastify.get('/antispam/:id', route$GetAntispam) + fastify.get('/raid/:id', route$GetRaidAsText) + fastify.get('/raid/:id.json', route$GetRaidAsJson) +} \ No newline at end of file diff --git a/milk/src/routes/get_raid/get_antispam.ts b/milk/src/routes/get_raid/get_antispam.ts new file mode 100644 index 0000000..4962ab4 --- /dev/null +++ b/milk/src/routes/get_raid/get_antispam.ts @@ -0,0 +1,7 @@ +import {FastifyReply, FastifyRequest} from "fastify"; +import {RaidParameter} from "../get_raid.js"; + +export async function route$GetAntispam(request: FastifyRequest, reply: FastifyReply): Promise { + let { id } = request.params + return reply.redirect('/raid/' + encodeURIComponent(id)) +} \ No newline at end of file diff --git a/milk/src/routes/get_raid/get_raid_as_json.ts b/milk/src/routes/get_raid/get_raid_as_json.ts new file mode 100644 index 0000000..142bd18 --- /dev/null +++ b/milk/src/routes/get_raid/get_raid_as_json.ts @@ -0,0 +1,49 @@ +import {FastifyReply, FastifyRequest} from "fastify"; +import {useCacheWhenPossible} from "../../fastify/serve_cached.js"; +import {getRaidByPublicId} from "../../database/raid.js"; +import {CursoredRaidParameter} from "../get_raid.js"; +import {paginateUsers} from "../../database/raid_users.js"; +import {PAGINATION_LIMIT} from "../../constants/pagination.js"; + +export async function route$GetRaidAsJson(request: FastifyRequest, reply: FastifyReply): Promise { + let { id } = request.params + + let cursorQuery = request.query.cursor + let cursor: Date | null = null + + if (cursorQuery != null) { + try { + cursor = new Date(cursorQuery) + } catch (ex) { + reply.code(400).send() + } + } + + const cacheKey = `${id}.json$cursor=${cursor?.toISOString() ?? 'null'}` + return await useCacheWhenPossible(reply, cacheKey, 'application/json', async (discard) => { + const raid = await getRaidByPublicId(id) + + if (raid == null) { + reply.code(404).send('404 Not Found') + return discard + } + + const { users, count } = await paginateUsers(raid.id, PAGINATION_LIMIT, cursor) + return { + result: JSON.stringify({ + raid: { + startedAt: raid.created_at, + concludedAt: raid.concluded_at, + guild: raid.guild_id.toString(), + size: count + }, + users: { + next: users.at(users.length - 1)?.joinedAt ?? null, + size: users.length, + data: users + }, + }), + shouldCache: users.length >= PAGINATION_LIMIT + } + }) +} \ No newline at end of file diff --git a/milk/src/routes/get_raid/get_raid_as_text.ts b/milk/src/routes/get_raid/get_raid_as_text.ts new file mode 100644 index 0000000..7fc94e5 --- /dev/null +++ b/milk/src/routes/get_raid/get_raid_as_text.ts @@ -0,0 +1,67 @@ +import {FastifyReply, FastifyRequest} from "fastify"; +import {useCacheWhenPossible} from "../../fastify/serve_cached.js"; +import {getRaidByPublicId} from "../../database/raid.js"; +import {toDateString, toTimeString} from "../../utils/date.js"; +import {Logger} from "@beemobot/common"; +import {TAG} from "../../constants/logging.js"; +import {RaidParameter} from "../get_raid.js"; +import {paginateUsers} from "../../database/raid_users.js"; +import {JSON_API_DOCUMENTATIONS} from "../../constants/links.js"; + +const numberFormatter = new Intl.NumberFormat('en-US') + +export async function route$GetRaidAsText(request: FastifyRequest, reply: FastifyReply): Promise { + let { id } = request.params + return await useCacheWhenPossible(reply, id, 'text', async (discard) => { + const raid = await getRaidByPublicId(id) + + if (raid == null) { + reply.code(404).send('404 Not Found') + return discard + } + + const { users, count } = await paginateUsers(raid.id, 2_000, null) + let response: string + let startedDate = toDateString(raid.created_at) + + response = 'Userbot raid detected against server ' + raid.guild_id + ' on ' + startedDate; + + if (users.length === 0) { + Logger.warn(TAG, `Raid ${id} reported no users.`) + response += "\nThere are no users logged for this raid, at this moment. It is likely that the raid is still being processed, please come back later!" + } else { + response += '\nRaid size: ' + numberFormatter.format(count) + ' accounts' + if (count > 2_000) { + response += `\nTo view beyond 2,000 accounts, see ${JSON_API_DOCUMENTATIONS} for more information.` + } + response += '\n' + response += '\n Joined at: ID: Username:' + response += '\n' + let userIds = ''; + for (const user of users) { + response += '\n' + response += toTimeString(user.joinedAt) + " " + user.id + spaces((18 - user.id.toString().length + 3)) + user.name + + userIds += '\n' + userIds += user.id + } + + response += '\n' + response += '\n Raw IDs:' + response += '\n' + response += userIds + } + return { + result: response, + shouldCache: raid.concluded_at != null || users.length > 2_000 + } + }) +} + +function spaces(num: number): string { + let whitespace = "" + while (whitespace.length < num) { + whitespace += " " + } + return whitespace +} \ No newline at end of file diff --git a/milk/src/types/fastify.ts b/milk/src/types/fastify.ts deleted file mode 100644 index 2c606de..0000000 --- a/milk/src/types/fastify.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {FastifyInstance} from "fastify"; - -export type Attachable = { - attach: (server: FastifyInstance) => any -} \ No newline at end of file diff --git a/milk/src/types/raid.ts b/milk/src/types/raid.ts new file mode 100644 index 0000000..718a957 --- /dev/null +++ b/milk/src/types/raid.ts @@ -0,0 +1,32 @@ +export type RaidManagementData = { + request: RaidManagementRequest | null, + response: RaidManagementResponse | null +} + +export type RaidManagementRequest = { + raidId: string, + guildId: string, + users: RaidManagementUser[], + concludedAtMillis: number | null +} + +export type RaidManagementResponse = { + publicId: string | null, + acknowledged: boolean +} + +export type RaidManagementUser = { + id: string, + name: string, + avatarHash: string | null, + createdAtMillis: number, + joinedAtMillis: number +} + +export type PublicRaidUser = { + id: string, + name: string, + avatarHash: string | null, + createdAt: Date, + joinedAt: Date +} \ No newline at end of file diff --git a/milk/src/utils/date.ts b/milk/src/utils/date.ts index f460395..b0b5c81 100644 --- a/milk/src/utils/date.ts +++ b/milk/src/utils/date.ts @@ -1,15 +1,13 @@ -const toDateString = (date: Date) => date.getUTCFullYear() + '/' +export const toDateString = (date: Date) => date.getUTCFullYear() + '/' + pad(2, date.getUTCMonth() + 1) + '/' + pad(2, date.getUTCDate()) const pad = (length: number = 2, number: number) => number.toString().padStart(length, "0") -const toISOString = (date: Date) => date.toISOString().replace('Z', '+0000') -const toTimeString = (date: Date) => +export const toISOString = (date: Date) => date.toISOString().replace('Z', '+0000') +export const toTimeString = (date: Date) => pad(2, date.getUTCHours()) + ':' + pad(2, date.getUTCMinutes()) + ':' + pad(2, date.getUTCSeconds()) + '.' + pad(3, date.getUTCMilliseconds()) + - '+0000' - -export const DateUtil = { toDateString: toDateString, toISOString: toISOString, toTimeString: toTimeString } \ No newline at end of file + '+0000' \ No newline at end of file diff --git a/milk/src/utils/number.ts b/milk/src/utils/number.ts index 2546edb..f78d914 100644 --- a/milk/src/utils/number.ts +++ b/milk/src/utils/number.ts @@ -1,7 +1,5 @@ -const random = (min: number, max: number): number => { +export const randomNumber = (min: number, max: number): number => { min = Math.ceil(min) max = Math.floor(max) return Math.floor(Math.random() * (max - min) + min) -} - -export const NumberUtil = { random: random } \ No newline at end of file +} \ No newline at end of file diff --git a/milk/src/utils/retriable.ts b/milk/src/utils/retriable.ts deleted file mode 100644 index 062d69c..0000000 --- a/milk/src/utils/retriable.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as Sentry from '@sentry/node'; -import {Logger} from "@beemobot/common"; -// ^ This needs to be updated; Probably @beemobot/cafe -import {TAG} from "../index.js"; - -export function retriable(task: string, action: () => Promise, retryEverySeconds: number = 10, maxRetries: number = -1, retries: number = 1): Promise { - return action() - .catch(async (exception) => { - if (retries === 1) { - Sentry.captureException(exception) - } - - if (maxRetries !== -1 && retries >= maxRetries) { - return exception - } - - Logger.error(TAG, 'Failed to complete ' + task + '. Retrying in ' + (retryEverySeconds * retries) + ' seconds.\n', exception) - await new Promise((resolve) => setTimeout(resolve, (retryEverySeconds * retries) * 1000)) - return retriable(task, action, retryEverySeconds, retries + 1) - }) -} - diff --git a/milk/src/utils/retry.ts b/milk/src/utils/retry.ts new file mode 100644 index 0000000..5a278d7 --- /dev/null +++ b/milk/src/utils/retry.ts @@ -0,0 +1,38 @@ +import * as Sentry from '@sentry/node'; +import {Logger} from "@beemobot/common"; +// ^ This needs to be updated; Probably @beemobot/cafe + +import {TAG} from "../constants/logging.js"; + +export async function run( + taskName: string, + action: () => Promise, + retryEverySeconds: number = 2, + maxRetries: number = 25, + retries: number = 1 +): Promise { + try { + return await action(); + } catch (exception) { + // Raise exception once to Sentry, we don't want to possibly send so many exceptions + if (retries === 1) { + Sentry.captureException(exception) + } + + if (maxRetries !== -1 && retries >= maxRetries) { + throw exception + } + + const secondsTillRetry = (retryEverySeconds * retries) + const logMessage = `Failed to complete ${taskName}. Retrying in ${secondsTillRetry} seconds.` + if (retries === 1) { + Logger.error(TAG, logMessage, exception) + } else { + Logger.error(TAG, logMessage) + } + + await new Promise((resolve) => setTimeout(resolve, secondsTillRetry * 1000)) + return run(taskName, action, retryEverySeconds, retries + 1) + } +} + diff --git a/milk/src/utils/string.ts b/milk/src/utils/string.ts index 21e23dd..9d639bb 100644 --- a/milk/src/utils/string.ts +++ b/milk/src/utils/string.ts @@ -1,28 +1,26 @@ -import {NumberUtil} from "./number.js"; +import {randomNumber} from "./number.js"; -export const random = (length: number): string => { +export const randomString = (length: number): string => { let contents = ''; while (contents.length < length) { - let seed = NumberUtil.random(1, 4); + let seed = randomNumber(1, 4); switch (seed) { case 1: { - seed = NumberUtil.random(65, 91); + seed = randomNumber(65, 91); contents += String.fromCharCode(seed); break; } case 2: { - seed = NumberUtil.random(97, 123); + seed = randomNumber(97, 123); contents += String.fromCharCode(seed); break; } case 3: { - seed = NumberUtil.random(0, 10); + seed = randomNumber(0, 10); contents += seed; break; } } } return contents -} - -export const StringUtil = { random: random } \ No newline at end of file +} \ No newline at end of file diff --git a/milk/yarn.lock b/milk/yarn.lock index f7f6495..7a984b2 100644 --- a/milk/yarn.lock +++ b/milk/yarn.lock @@ -4,14 +4,14 @@ "@beemobot/common@^1.0.0": version "1.0.0" - resolved "https://registry.yarnpkg.com/@beemobot/common/-/common-1.0.0.tgz#d7b4ef1c47d9d3974b81433760ca70aa7cdb8167" + resolved "https://registry.npmjs.org/@beemobot/common/-/common-1.0.0.tgz" integrity sha512-7cnly7AIW+Ag1/NfQ8Ji1o6WcXLecDQDYLR18GwFhf2k9Hva9oZo6u2e/qAed0Ixij1/pJzE53DZHAczgvQaSA== dependencies: kafkajs "^2.2.3" "@fastify/ajv-compiler@^3.3.1": version "3.5.0" - resolved "https://registry.yarnpkg.com/@fastify/ajv-compiler/-/ajv-compiler-3.5.0.tgz#459bff00fefbf86c96ec30e62e933d2379e46670" + resolved "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-3.5.0.tgz" integrity sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA== dependencies: ajv "^8.11.0" @@ -20,24 +20,24 @@ "@fastify/deepmerge@^1.0.0": version "1.3.0" - resolved "https://registry.yarnpkg.com/@fastify/deepmerge/-/deepmerge-1.3.0.tgz#8116858108f0c7d9fd460d05a7d637a13fe3239a" + resolved "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-1.3.0.tgz" integrity sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A== "@fastify/error@^3.0.0": version "3.2.0" - resolved "https://registry.yarnpkg.com/@fastify/error/-/error-3.2.0.tgz#9010e0acfe07965f5fc7d2b367f58f042d0f4106" + resolved "https://registry.npmjs.org/@fastify/error/-/error-3.2.0.tgz" integrity sha512-KAfcLa+CnknwVi5fWogrLXgidLic+GXnLjijXdpl8pvkvbXU5BGa37iZO9FGvsh9ZL4y+oFi5cbHBm5UOG+dmQ== "@fastify/fast-json-stringify-compiler@^4.1.0": version "4.2.0" - resolved "https://registry.yarnpkg.com/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.2.0.tgz#52d047fac76b0d75bd660f04a5dd606659f57c5a" + resolved "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-4.2.0.tgz" integrity sha512-ypZynRvXA3dibfPykQN3RB5wBdEUgSGgny8Qc6k163wYPLD4mEGEDkACp+00YmqkGvIm8D/xYoHajwyEdWD/eg== dependencies: fast-json-stringify "^5.0.0" "@isaacs/cliui@^8.0.2": version "8.0.2" - resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz" integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== dependencies: string-width "^5.1.2" @@ -49,29 +49,29 @@ "@pkgjs/parseargs@^0.11.0": version "0.11.0" - resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== -"@prisma/client@4.8.1": - version "4.8.1" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.8.1.tgz#51c16488dfac4e74a275a2753bf20262a65f2a2b" - integrity sha512-d4xhZhETmeXK/yZ7K0KcVOzEfI5YKGGEr4F5SBV04/MU4ncN/HcE28sy3e4Yt8UFW0ZuImKFQJE+9rWt9WbGSQ== +"@prisma/client@^5.5.2": + version "5.5.2" + resolved "https://registry.npmjs.org/@prisma/client/-/client-5.5.2.tgz" + integrity sha512-54XkqR8M+fxbzYqe+bIXimYnkkcGqgOh0dn0yWtIk6CQT4IUCAvNFNcQZwk2KqaLU+/1PHTSWrcHtx4XjluR5w== dependencies: - "@prisma/engines-version" "4.8.0-61.d6e67a83f971b175a593ccc12e15c4a757f93ffe" + "@prisma/engines-version" "5.5.1-1.aebc046ce8b88ebbcb45efe31cbe7d06fd6abc0a" -"@prisma/engines-version@4.8.0-61.d6e67a83f971b175a593ccc12e15c4a757f93ffe": - version "4.8.0-61.d6e67a83f971b175a593ccc12e15c4a757f93ffe" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.8.0-61.d6e67a83f971b175a593ccc12e15c4a757f93ffe.tgz#30401aba1029e7d32e3cb717e705a7c92ccc211e" - integrity sha512-MHSOSexomRMom8QN4t7bu87wPPD+pa+hW9+71JnVcF3DqyyO/ycCLhRL1we3EojRpZxKvuyGho2REQsMCvxcJw== +"@prisma/engines-version@5.5.1-1.aebc046ce8b88ebbcb45efe31cbe7d06fd6abc0a": + version "5.5.1-1.aebc046ce8b88ebbcb45efe31cbe7d06fd6abc0a" + resolved "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.5.1-1.aebc046ce8b88ebbcb45efe31cbe7d06fd6abc0a.tgz" + integrity sha512-O+qHFnZvAyOFk1tUco2/VdiqS0ym42a3+6CYLScllmnpbyiTplgyLt2rK/B9BTjYkSHjrgMhkG47S0oqzdIckA== -"@prisma/engines@4.8.1": - version "4.8.1" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.8.1.tgz#8428f7dcd7912c6073024511376595017630dc85" - integrity sha512-93tctjNXcIS+i/e552IO6tqw17sX8liivv8WX9lDMCpEEe3ci+nT9F+1oHtAafqruXLepKF80i/D20Mm+ESlOw== +"@prisma/engines@5.5.2": + version "5.5.2" + resolved "https://registry.npmjs.org/@prisma/engines/-/engines-5.5.2.tgz" + integrity sha512-Be5hoNF8k+lkB3uEMiCHbhbfF6aj1GnrTBnn5iYFT7GEr3TsOEp1soviEcBR0tYCgHbxjcIxJMhdbvxALJhAqg== "@sentry/core@7.29.0": version "7.29.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.29.0.tgz#bc4b54d56cf7652598d4430cf43ea97cc069f6fe" + resolved "https://registry.npmjs.org/@sentry/core/-/core-7.29.0.tgz" integrity sha512-+e9aIp2ljtT4EJq3901z6TfEVEeqZd5cWzbKEuQzPn2UO6If9+Utd7kY2Y31eQYb4QnJgZfiIEz1HonuYY6zqQ== dependencies: "@sentry/types" "7.29.0" @@ -80,7 +80,7 @@ "@sentry/node@^7.29.0": version "7.29.0" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.29.0.tgz#721aab15faef98f291b5a3fcb9b303565deb1e74" + resolved "https://registry.npmjs.org/@sentry/node/-/node-7.29.0.tgz" integrity sha512-s/bN/JS5gPTmwzVms4FtI5YNYtC9aGY4uqdx/llVrIiVv7G6md/oJJzKtO1C4dt6YshjGjSs5KCpEn1NM4+1iA== dependencies: "@sentry/core" "7.29.0" @@ -93,12 +93,12 @@ "@sentry/types@7.29.0": version "7.29.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.29.0.tgz#ed829b6014ee19049035fec6af2b4fea44ff28b8" + resolved "https://registry.npmjs.org/@sentry/types/-/types-7.29.0.tgz" integrity sha512-DmoEpoqHPty3VxqubS/5gxarwebHRlcBd/yuno+PS3xy++/i9YPjOWLZhU2jYs1cW68M9R6CcCOiC9f2ckJjdw== "@sentry/utils@7.29.0": version "7.29.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.29.0.tgz#cbf8f87dd851b0fdc7870db9c68014c321c3bab8" + resolved "https://registry.npmjs.org/@sentry/utils/-/utils-7.29.0.tgz" integrity sha512-ICcBwTiBGK8NQA8H2BJo0JcMN6yCeKLqNKNMVampRgS6wSfSk1edvcTdhRkW3bSktIGrIPZrKskBHyMwDGF2XQ== dependencies: "@sentry/types" "7.29.0" @@ -106,38 +106,38 @@ "@types/node@^18.11.18": version "18.11.18" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f" + resolved "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz" integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA== abort-controller@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + resolved "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz" integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== dependencies: event-target-shim "^5.0.0" abstract-logging@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/abstract-logging/-/abstract-logging-2.0.1.tgz#6b0c371df212db7129b57d2e7fcf282b8bf1c839" + resolved "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz" integrity sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA== agent-base@6: version "6.0.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== dependencies: debug "4" ajv-formats@^2.1.1: version "2.1.1" - resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + resolved "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz" integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== dependencies: ajv "^8.0.0" ajv@^8.0.0, ajv@^8.10.0, ajv@^8.11.0: version "8.12.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" + resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== dependencies: fast-deep-equal "^3.1.1" @@ -147,39 +147,39 @@ ajv@^8.0.0, ajv@^8.10.0, ajv@^8.11.0: ansi-regex@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== ansi-regex@^6.0.1: version "6.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz" integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== ansi-styles@^4.0.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: color-convert "^2.0.1" ansi-styles@^6.1.0: version "6.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== archy@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" + resolved "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz" integrity sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw== atomic-sleep@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" + resolved "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz" integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== avvio@^8.2.0: version "8.2.0" - resolved "https://registry.yarnpkg.com/avvio/-/avvio-8.2.0.tgz#aff28b0266617bf07ffc1c2d5f4220c3663ce1c2" + resolved "https://registry.npmjs.org/avvio/-/avvio-8.2.0.tgz" integrity sha512-bbCQdg7bpEv6kGH41RO/3B2/GMMmJSo2iBK+X8AWN9mujtfUipMDfIjsgHCfpnKqoGEQrrmCDKSa5OQ19+fDmg== dependencies: archy "^1.0.0" @@ -188,24 +188,24 @@ avvio@^8.2.0: balanced-match@^1.0.0: version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== base64-js@^1.3.1: version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== brace-expansion@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz" integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== dependencies: balanced-match "^1.0.0" buffer@^6.0.3: version "6.0.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz" integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== dependencies: base64-js "^1.3.1" @@ -213,95 +213,95 @@ buffer@^6.0.3: clone@2.x: version "2.1.2" - resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + resolved "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz" integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== color-convert@^2.0.1: version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== dependencies: color-name "~1.1.4" color-name@~1.1.4: version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== content-type@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== cookie@^0.4.1: version "0.4.2" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz" integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== cookie@^0.5.0: version "0.5.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + resolved "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== cross-spawn@^7.0.0: version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== dependencies: path-key "^3.1.0" shebang-command "^2.0.0" which "^2.0.1" -debug@4, debug@^4.0.0: +debug@^4.0.0, debug@4: version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" dotenv@^16.0.3: version "16.0.3" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" + resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz" integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== eastasianwidth@^0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== emoji-regex@^8.0.0: version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== emoji-regex@^9.2.2: version "9.2.2" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== event-target-shim@^5.0.0: version "5.0.1" - resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + resolved "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== events@^3.3.0: version "3.3.0" - resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== fast-decode-uri-component@^1.0.1: version "1.0.1" - resolved "https://registry.yarnpkg.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz#46f8b6c22b30ff7a81357d4f59abfae938202543" + resolved "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz" integrity sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg== fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-json-stringify@^5.0.0: version "5.5.0" - resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-5.5.0.tgz#6655cb944df8da43f6b15312a9564b81c55dadab" + resolved "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.5.0.tgz" integrity sha512-rmw2Z8/mLkND8zI+3KTYIkNPEoF5v6GqDP/o+g7H3vjdWjBwuKpgAYFHIzL6ORRB+iqDjjtJnLIW9Mzxn5szOA== dependencies: "@fastify/deepmerge" "^1.0.0" @@ -313,24 +313,29 @@ fast-json-stringify@^5.0.0: fast-querystring@^1.0.0: version "1.1.0" - resolved "https://registry.yarnpkg.com/fast-querystring/-/fast-querystring-1.1.0.tgz#bb645c365db88a3b6433fb6484f7e9e66764cfb9" + resolved "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.0.tgz" integrity sha512-LWkjBCZlxjnSanuPpZ6mHswjy8hQv3VcPJsQB3ltUF2zjvrycr0leP3TSTEEfvQ1WEMSRl5YNsGqaft9bjLqEw== dependencies: fast-decode-uri-component "^1.0.1" fast-redact@^3.1.1: version "3.1.2" - resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-3.1.2.tgz#d58e69e9084ce9fa4c1a6fa98a3e1ecf5d7839aa" + resolved "https://registry.npmjs.org/fast-redact/-/fast-redact-3.1.2.tgz" integrity sha512-+0em+Iya9fKGfEQGcd62Yv6onjBmmhV1uh86XVfOU8VwAe6kaFdQCWI9s0/Nnugx5Vd9tdbZ7e6gE2tR9dzXdw== fast-uri@^2.0.0, fast-uri@^2.1.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-2.2.0.tgz#519a0f849bef714aad10e9753d69d8f758f7445a" + resolved "https://registry.npmjs.org/fast-uri/-/fast-uri-2.2.0.tgz" integrity sha512-cIusKBIt/R/oI6z/1nyfe2FvGKVTohVRfvkOhvx0nCEW+xf5NoCXjAHcWp93uOUBchzYcsvPlrapAdX1uW+YGg== +fastify-plugin@^4.5.1: + version "4.5.1" + resolved "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz" + integrity sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ== + fastify@^4.11.0: version "4.11.0" - resolved "https://registry.yarnpkg.com/fastify/-/fastify-4.11.0.tgz#7fa5614c81a618e67a7a467f0f1b33c43f4ff7d2" + resolved "https://registry.npmjs.org/fastify/-/fastify-4.11.0.tgz" integrity sha512-JteZ8pjEqd+6n+azQnQfSJV8MUMxAmxbvC2Dx/Mybj039Lf/u3kda9Kq84uy/huCpqCzZoyHIZS5JFGF3wLztw== dependencies: "@fastify/ajv-compiler" "^3.3.1" @@ -351,14 +356,14 @@ fastify@^4.11.0: fastq@^1.6.1: version "1.15.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" + resolved "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz" integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== dependencies: reusify "^1.0.4" find-my-way@^7.3.0: version "7.4.0" - resolved "https://registry.yarnpkg.com/find-my-way/-/find-my-way-7.4.0.tgz#22363e6cd1c466f88883703e169a20c983f9c9cc" + resolved "https://registry.npmjs.org/find-my-way/-/find-my-way-7.4.0.tgz" integrity sha512-JFT7eURLU5FumlZ3VBGnveId82cZz7UR7OUu+THQJOwdQXxmS/g8v0KLoFhv97HreycOrmAbqjXD/4VG2j0uMQ== dependencies: fast-deep-equal "^3.1.3" @@ -367,7 +372,7 @@ find-my-way@^7.3.0: foreground-child@^3.1.0: version "3.1.1" - resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" + resolved "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz" integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== dependencies: cross-spawn "^7.0.0" @@ -375,12 +380,12 @@ foreground-child@^3.1.0: forwarded@0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== glob@^10.2.5: version "10.2.6" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.2.6.tgz#1e27edbb3bbac055cb97113e27a066c100a4e5e1" + resolved "https://registry.npmjs.org/glob/-/glob-10.2.6.tgz" integrity sha512-U/rnDpXJGF414QQQZv5uVsabTVxMSwzS5CH0p3DRCIV6ownl4f7PzGnkGmvlum2wB+9RlJWJZ6ACU1INnBqiPA== dependencies: foreground-child "^3.1.0" @@ -391,7 +396,7 @@ glob@^10.2.5: 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" + resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== dependencies: agent-base "6" @@ -399,27 +404,27 @@ https-proxy-agent@^5.0.0: ieee754@^1.2.1: version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== ipaddr.js@1.9.1: version "1.9.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== is-fullwidth-code-point@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== isexe@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== jackspeak@^2.0.3: version "2.2.1" - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.2.1.tgz#655e8cf025d872c9c03d3eb63e8f0c024fef16a6" + resolved "https://registry.npmjs.org/jackspeak/-/jackspeak-2.2.1.tgz" integrity sha512-MXbxovZ/Pm42f6cDIDkl3xpwv1AGwObKwfmjs2nQePiy85tP3fatofl3FC1aBsOtP/6fq5SbtgHwWcMsLP+bDw== dependencies: "@isaacs/cliui" "^8.0.2" @@ -428,77 +433,77 @@ jackspeak@^2.0.3: json-schema-traverse@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== kafkajs@^2.2.3: version "2.2.3" - resolved "https://registry.yarnpkg.com/kafkajs/-/kafkajs-2.2.3.tgz#a1ab1b7c4a27699871a89b3978b5cfe5b05c6f3e" + resolved "https://registry.npmjs.org/kafkajs/-/kafkajs-2.2.3.tgz" integrity sha512-JmzIiLHE/TdQ7b4I2B/DNMtfhTh66fmEaEg7gGkyQXBC6f1A7I2jSjeUsVIJfC8d9YcEIURyBjtOEKBO5OHVhg== light-my-request@^5.6.1: version "5.8.0" - resolved "https://registry.yarnpkg.com/light-my-request/-/light-my-request-5.8.0.tgz#93b28615d4cd134b4e2370bcf2ff7e35b51c8d29" + resolved "https://registry.npmjs.org/light-my-request/-/light-my-request-5.8.0.tgz" integrity sha512-4BtD5C+VmyTpzlDPCZbsatZMJVgUIciSOwYhJDCbLffPZ35KoDkDj4zubLeHDEb35b4kkPeEv5imbh+RJxK/Pg== dependencies: cookie "^0.5.0" process-warning "^2.0.0" set-cookie-parser "^2.4.1" +lru_map@^0.3.3: + version "0.3.3" + resolved "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz" + integrity sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ== + lru-cache@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== dependencies: yallist "^4.0.0" lru-cache@^9.1.1: version "9.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-9.1.1.tgz#c58a93de58630b688de39ad04ef02ef26f1902f1" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-9.1.1.tgz" integrity sha512-65/Jky17UwSb0BuB9V+MyDpsOtXKmYwzhyl+cOa9XUiI4uV2Ouy/2voFP3+al0BjZbJgMBD8FojMpAf+Z+qn4A== -lru_map@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" - integrity sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ== - minimatch@^9.0.1: version "9.0.1" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.1.tgz#8a555f541cf976c622daf078bb28f29fb927c253" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz" integrity sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w== dependencies: brace-expansion "^2.0.1" "minipass@^5.0.0 || ^6.0.2": version "6.0.2" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-6.0.2.tgz#542844b6c4ce95b202c0995b0a471f1229de4c81" + resolved "https://registry.npmjs.org/minipass/-/minipass-6.0.2.tgz" integrity sha512-MzWSV5nYVT7mVyWCwn2o7JH13w2TBRmmSqSRCKzTw+lmft9X4z+3wjvs06Tzijo5z4W/kahUCDpRXTF+ZrmF/w== ms@2.1.2: version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== node-cache@^5.1.2: version "5.1.2" - resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d" + resolved "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz" integrity sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg== dependencies: clone "2.x" on-exit-leak-free@^2.1.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz#5c703c968f7e7f851885f6459bf8a8a57edc9cc4" + resolved "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz" integrity sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w== path-key@^3.1.0: version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== path-scurry@^1.7.0: version "1.9.2" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.9.2.tgz#90f9d296ac5e37e608028e28a447b11d385b3f63" + resolved "https://registry.npmjs.org/path-scurry/-/path-scurry-1.9.2.tgz" integrity sha512-qSDLy2aGFPm8i4rsbHd4MNyTcrzHFsLQykrtbuGRknZZCBBVXSv2tSCDN2Cg6Rt/GFRw8GoW9y9Ecw5rIPG1sg== dependencies: lru-cache "^9.1.1" @@ -506,7 +511,7 @@ path-scurry@^1.7.0: pino-abstract-transport@v1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz#cc0d6955fffcadb91b7b49ef220a6cc111d48bb3" + resolved "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz" integrity sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA== dependencies: readable-stream "^4.0.0" @@ -514,12 +519,12 @@ pino-abstract-transport@v1.0.0: pino-std-serializers@^6.0.0: version "6.1.0" - resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-6.1.0.tgz#307490fd426eefc95e06067e85d8558603e8e844" + resolved "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.1.0.tgz" integrity sha512-KO0m2f1HkrPe9S0ldjx7za9BJjeHqBku5Ch8JyxETxT8dEFGz1PwgrHaOQupVYitpzbFSYm7nnljxD8dik2c+g== pino@^8.5.0: version "8.8.0" - resolved "https://registry.yarnpkg.com/pino/-/pino-8.8.0.tgz#1f0d6695a224aa06afc7ad60f2ccc4772d3b9233" + resolved "https://registry.npmjs.org/pino/-/pino-8.8.0.tgz" integrity sha512-cF8iGYeu2ODg2gIwgAHcPrtR63ILJz3f7gkogaHC/TXVVXxZgInmNYiIpDYEwgEkxZti2Se6P2W2DxlBIZe6eQ== dependencies: atomic-sleep "^1.0.0" @@ -534,26 +539,26 @@ pino@^8.5.0: sonic-boom "^3.1.0" thread-stream "^2.0.0" -prisma@^4.8.1: - version "4.8.1" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.8.1.tgz#ef93cd908809b7d02e9f4bead5eea7733ba50c68" - integrity sha512-ZMLnSjwulIeYfaU1O6/LF6PEJzxN5par5weykxMykS9Z6ara/j76JH3Yo2AH3bgJbPN4Z6NeCK9s5fDkzf33cg== +prisma@*, prisma@^5.5.2: + version "5.5.2" + resolved "https://registry.npmjs.org/prisma/-/prisma-5.5.2.tgz" + integrity sha512-WQtG6fevOL053yoPl6dbHV+IWgKo25IRN4/pwAGqcWmg7CrtoCzvbDbN9fXUc7QS2KK0LimHIqLsaCOX/vHl8w== dependencies: - "@prisma/engines" "4.8.1" + "@prisma/engines" "5.5.2" process-warning@^2.0.0: version "2.1.0" - resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-2.1.0.tgz#1e60e3bfe8183033bbc1e702c2da74f099422d1a" + resolved "https://registry.npmjs.org/process-warning/-/process-warning-2.1.0.tgz" integrity sha512-9C20RLxrZU/rFnxWncDkuF6O999NdIf3E1ws4B0ZeY3sRVPzWBMsYDE2lxjxhiXxg464cQTgKUGm8/i6y2YGXg== process@^0.11.10: version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== proxy-addr@^2.0.7: version "2.0.7" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz" integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== dependencies: forwarded "0.2.0" @@ -561,17 +566,17 @@ proxy-addr@^2.0.7: punycode@^2.1.0: version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== quick-format-unescaped@^4.0.3: version "4.0.4" - resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz#93ef6dd8d3453cbc7970dd614fad4c5954d6b5a7" + resolved "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz" integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== readable-stream@^4.0.0: version "4.3.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.3.0.tgz#0914d0c72db03b316c9733bb3461d64a3cc50cba" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-4.3.0.tgz" integrity sha512-MuEnA0lbSi7JS8XM+WNJlWZkHAAdm7gETHdFK//Q/mChGyj2akEFtdLZh32jSdkWGbRwCW9pn6g3LWDdDeZnBQ== dependencies: abort-controller "^3.0.0" @@ -581,97 +586,106 @@ readable-stream@^4.0.0: real-require@^0.2.0: version "0.2.0" - resolved "https://registry.yarnpkg.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78" + resolved "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz" integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== require-from-string@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== ret@~0.2.0: version "0.2.2" - resolved "https://registry.yarnpkg.com/ret/-/ret-0.2.2.tgz#b6861782a1f4762dce43402a71eb7a283f44573c" + resolved "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz" integrity sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ== reusify@^1.0.4: version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== rfdc@^1.2.0, rfdc@^1.3.0: version "1.3.0" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" + resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz" integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== rimraf@^5.0.1: version "5.0.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.1.tgz#0881323ab94ad45fec7c0221f27ea1a142f3f0d0" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-5.0.1.tgz" integrity sha512-OfFZdwtd3lZ+XZzYP/6gTACubwFcHdLRqS9UX3UwpU2dnGQYkPFISRwvM3w9IiB2w7bW5qGo/uAwE4SmXXSKvg== dependencies: glob "^10.2.5" safe-regex2@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/safe-regex2/-/safe-regex2-2.0.0.tgz#b287524c397c7a2994470367e0185e1916b1f5b9" + resolved "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz" integrity sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ== dependencies: ret "~0.2.0" safe-stable-stringify@^2.3.1: version "2.4.2" - resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.2.tgz#ec7b037768098bf65310d1d64370de0dc02353aa" + resolved "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.2.tgz" integrity sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA== secure-json-parse@^2.5.0: version "2.6.0" - resolved "https://registry.yarnpkg.com/secure-json-parse/-/secure-json-parse-2.6.0.tgz#95d89f84adf32d76ff7800e68a673b129fe918b0" + resolved "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.6.0.tgz" integrity sha512-B9osKohb6L+EZ6Kve3wHKfsAClzOC/iISA2vSuCe5Jx5NAKiwitfxx8ZKYapHXr0sYRj7UZInT7pLb3rp2Yx6A== semver@^7.3.7: version "7.3.8" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" + resolved "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz" integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== dependencies: lru-cache "^6.0.0" set-cookie-parser@^2.4.1: version "2.5.1" - resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.5.1.tgz#ddd3e9a566b0e8e0862aca974a6ac0e01349430b" + resolved "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.5.1.tgz" integrity sha512-1jeBGaKNGdEq4FgIrORu/N570dwoPYio8lSoYLWmX7sQ//0JY08Xh9o5pBcgmHQ/MbsYp/aZnOe1s1lIsbLprQ== shebang-command@^2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== dependencies: shebang-regex "^3.0.0" shebang-regex@^3.0.0: version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== signal-exit@^4.0.1: version "4.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.0.2.tgz#ff55bb1d9ff2114c13b400688fa544ac63c36967" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz" integrity sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q== sonic-boom@^3.1.0: version "3.2.1" - resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-3.2.1.tgz#972ceab831b5840a08a002fa95a672008bda1c38" + resolved "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.2.1.tgz" integrity sha512-iITeTHxy3B9FGu8aVdiDXUVAcHMF9Ss0cCsAOo2HfCrmVGT3/DT5oYaeu0M/YKZDlKTvChEyPq0zI9Hf33EX6A== dependencies: atomic-sleep "^1.0.0" split2@^4.0.0: version "4.1.0" - resolved "https://registry.yarnpkg.com/split2/-/split2-4.1.0.tgz#101907a24370f85bb782f08adaabe4e281ecf809" + resolved "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz" integrity sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0: + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: emoji-regex "^8.0.0" @@ -680,66 +694,73 @@ split2@^4.0.0: string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz" integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== dependencies: eastasianwidth "^0.2.0" emoji-regex "^9.2.2" strip-ansi "^7.0.1" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" strip-ansi@^7.0.1: version "7.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz" integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw== dependencies: ansi-regex "^6.0.1" thread-stream@^2.0.0: version "2.2.0" - resolved "https://registry.yarnpkg.com/thread-stream/-/thread-stream-2.2.0.tgz#310c03a253f729094ce5d4638ef5186dfa80a9e8" + resolved "https://registry.npmjs.org/thread-stream/-/thread-stream-2.2.0.tgz" integrity sha512-rUkv4/fnb4rqy/gGy7VuqK6wE1+1DOCOWy4RMeaV69ZHMP11tQKZvZSip1yTgrKCMZzEMcCL/bKfHvSfDHx+iQ== dependencies: real-require "^0.2.0" tiny-lru@^10.0.0: version "10.0.1" - resolved "https://registry.yarnpkg.com/tiny-lru/-/tiny-lru-10.0.1.tgz#aaf5d22207e641ed1b176ac2e616d6cc2fc9ef66" + resolved "https://registry.npmjs.org/tiny-lru/-/tiny-lru-10.0.1.tgz" integrity sha512-Vst+6kEsWvb17Zpz14sRJV/f8bUWKhqm6Dc+v08iShmIJ/WxqWytHzCTd6m88pS33rE2zpX34TRmOpAJPloNCA== tslib@^1.9.3: version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== typescript@^4.9.4: version "4.9.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78" + resolved "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz" integrity sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg== uri-js@^4.2.2: version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== dependencies: punycode "^2.1.0" which@^2.0.1: version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== dependencies: ansi-styles "^4.0.0" @@ -748,7 +769,7 @@ which@^2.0.1: wrap-ansi@^8.1.0: version "8.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== dependencies: ansi-styles "^6.1.0" @@ -757,5 +778,5 @@ wrap-ansi@^8.1.0: yallist@^4.0.0: version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==