From 4589bbf67925531a04834245b44df28a07a74c7b Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Mon, 20 May 2024 12:21:14 +0200 Subject: [PATCH] feat: maintain hashed blocklist of banned usernames, emails and discordids (fehmer) (#5371) * feat: maintain blocklist of banned usernames and email (fehmer) * update privacy policy --------- Co-authored-by: Miodec --- .../__tests__/api/controllers/user.spec.ts | 514 +++++++++++++-- backend/__tests__/dal/blocklist.spec.ts | 339 ++++++++++ backend/__tests__/global-setup.ts | 14 + backend/__tests__/setup-tests.ts | 122 ++-- backend/__tests__/utils/misc.spec.ts | 7 +- backend/package-lock.json | 618 ++++++++++-------- backend/package.json | 3 + backend/src/api/controllers/user.ts | 83 ++- backend/src/api/routes/admin.ts | 1 - backend/src/dal/blocklist.ts | 116 ++++ backend/src/server.ts | 4 + backend/src/types/types.d.ts | 10 + backend/src/utils/discord.ts | 29 +- backend/vitest.config.js | 1 + frontend/src/privacy-policy.html | 14 +- 15 files changed, 1453 insertions(+), 422 deletions(-) create mode 100644 backend/__tests__/dal/blocklist.spec.ts create mode 100644 backend/__tests__/global-setup.ts create mode 100644 backend/src/dal/blocklist.ts diff --git a/backend/__tests__/api/controllers/user.spec.ts b/backend/__tests__/api/controllers/user.spec.ts index c26a70725..45c607473 100644 --- a/backend/__tests__/api/controllers/user.spec.ts +++ b/backend/__tests__/api/controllers/user.spec.ts @@ -4,11 +4,35 @@ import * as Configuration from "../../../src/init/configuration"; import { getCurrentTestActivity } from "../../../src/api/controllers/user"; import * as UserDal from "../../../src/dal/user"; import _ from "lodash"; +import { DecodedIdToken } from "firebase-admin/lib/auth/token-verifier"; +import * as AuthUtils from "../../../src/utils/auth"; +import * as BlocklistDal from "../../../src/dal/blocklist"; +import * as ApeKeys from "../../../src/dal/ape-keys"; +import * as PresetDal from "../../../src/dal/preset"; +import * as ConfigDal from "../../../src/dal/config"; +import * as ResultDal from "../../../src/dal/result"; +import * as DailyLeaderboards from "../../../src/utils/daily-leaderboards"; +import GeorgeQueue from "../../../src/queues/george-queue"; +import * as AdminUuids from "../../../src/dal/admin-uids"; +import * as DiscordUtils from "../../../src/utils/discord"; const mockApp = request(app); +const configuration = Configuration.getCachedConfiguration(); + +const mockDecodedToken: DecodedIdToken = { + uid: "123456789", + email: "newuser@mail.com", + iat: Date.now(), +} as DecodedIdToken; describe("user controller test", () => { + beforeEach(() => { + vi.spyOn(AuthUtils, "verifyIdToken").mockResolvedValue(mockDecodedToken); + }); describe("user creation flow", () => { + beforeEach(async () => { + await enableSignup(true); + }); it("should be able to check name, sign up, and get user data", async () => { await mockApp .get("/users/checkName/NewUser") @@ -24,42 +48,6 @@ describe("user controller test", () => { captcha: "captcha", }; - vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue({ - //if stuff breaks this might be the reason - users: { - signUp: true, - discordIntegration: { - enabled: false, - }, - autoBan: { - enabled: false, - maxCount: 5, - maxHours: 1, - }, - profiles: { - enabled: false, - }, - xp: { - enabled: false, - gainMultiplier: 0, - maxDailyBonus: 0, - minDailyBonus: 0, - streak: { - enabled: false, - maxStreakDays: 0, - maxStreakMultiplier: 0, - }, - }, - inbox: { - enabled: false, - maxMail: 0, - }, - premium: { - enabled: true, - }, - }, - } as any); - await mockApp .post("/users/signup") .set("authorization", "Uid 123456789|newuser@mail.com") @@ -92,20 +80,133 @@ describe("user controller test", () => { Accept: "application/json", }) .expect(409); - - vi.restoreAllMocks(); }); }); + describe("user signup", () => { + const blocklistContainsMock = vi.spyOn(BlocklistDal, "contains"); + const firebaseDeleteUserMock = vi.spyOn(AuthUtils, "deleteUser"); + const usernameAvailableMock = vi.spyOn(UserDal, "isNameAvailable"); + beforeEach(async () => { + await enableSignup(true); + usernameAvailableMock.mockResolvedValue(true); + }); + afterEach(() => { + [ + blocklistContainsMock, + firebaseDeleteUserMock, + usernameAvailableMock, + ].forEach((it) => it.mockReset()); + }); + it("should not create user if blocklisted", async () => { + //GIVEN + blocklistContainsMock.mockResolvedValue(true); + firebaseDeleteUserMock.mockResolvedValue(); + + const newUser = { + name: "NewUser", + uid: "123456789", + email: "newuser@mail.com", + captcha: "captcha", + }; + + //WHEN + const result = await mockApp + .post("/users/signup") + .set("authorization", "Uid 123456789|newuser@mail.com") + .send(newUser) + .set({ + Accept: "application/json", + }) + .expect(409); + + //THEN + expect(result.body.message).toEqual("Username or email blocked"); + expect(blocklistContainsMock).toHaveBeenCalledWith({ + name: "NewUser", + email: "newuser@mail.com", + }); + + //user will be created in firebase from the frontend, make sure we remove it + expect(firebaseDeleteUserMock).toHaveBeenCalledWith("123456789"); + }); + + it("should not create user domain is blacklisted", async () => { + ["tidal.lol", "selfbot.cc"].forEach(async (domain) => { + //GIVEN + firebaseDeleteUserMock.mockResolvedValue(); + + const newUser = { + name: "NewUser", + uid: "123456789", + email: `newuser@${domain}`, + captcha: "captcha", + }; + + //WHEN + const result = await mockApp + .post("/users/signup") + .set("authorization", `Uid 123456789|newuser@${domain}`) + .send(newUser) + .set({ + Accept: "application/json", + }) + .expect(400); + + //THEN + expect(result.body.message).toEqual("Invalid domain"); + + //user will be created in firebase from the frontend, make sure we remove it + expect(firebaseDeleteUserMock).toHaveBeenCalledWith("123456789"); + }); + }); + + it("should not create user if username is taken", async () => { + //GIVEN + usernameAvailableMock.mockResolvedValue(false); + firebaseDeleteUserMock.mockResolvedValue(); + + const newUser = { + name: "NewUser", + uid: "123456789", + email: "newuser@mail.com", + captcha: "captcha", + }; + + //WHEN + const result = await mockApp + .post("/users/signup") + .set("authorization", "Uid 123456789|newuser@mail.com") + .send(newUser) + .set({ + Accept: "application/json", + }) + .expect(409); + + //THEN + expect(result.body.message).toEqual("Username unavailable"); + expect(usernameAvailableMock).toHaveBeenCalledWith( + "NewUser", + "123456789" + ); + + //user will be created in firebase from the frontend, make sure we remove it + expect(firebaseDeleteUserMock).toHaveBeenCalledWith("123456789"); + }); + }); describe("getTestActivity", () => { + const getUserMock = vi.spyOn(UserDal, "getUser"); + afterAll(() => { + getUserMock.mockReset(); + }); it("should return 503 for non premium users", async () => { //given - vi.spyOn(UserDal, "getUser").mockResolvedValue({ + getUserMock.mockResolvedValue({ testActivity: { "2023": [1, 2, 3], "2024": [4, 5, 6] }, } as unknown as MonkeyTypes.DBUser); //when - const response = await mockApp + await mockApp .get("/users/testActivity") .set("authorization", "Uid 123456789") .send() @@ -113,7 +214,7 @@ describe("user controller test", () => { }); it("should send data for premium users", async () => { //given - vi.spyOn(UserDal, "getUser").mockResolvedValue({ + getUserMock.mockResolvedValue({ testActivity: { "2023": [1, 2, 3], "2024": [4, 5, 6] }, } as unknown as MonkeyTypes.DBUser); vi.spyOn(UserDal, "checkIfUserIsPremium").mockResolvedValue(true); @@ -200,6 +301,305 @@ describe("user controller test", () => { expect(testsByDays[365]).toEqual(2024094); //2024-01 }); }); + + describe("toggle ban", () => { + const getUserMock = vi.spyOn(UserDal, "getUser"); + const setBannedMock = vi.spyOn(UserDal, "setBanned"); + const georgeUserBannedMock = vi.spyOn(GeorgeQueue, "userBanned"); + const isAdminMock = vi.spyOn(AdminUuids, "isAdmin"); + beforeEach(async () => { + await enableAdminFeatures(true); + + isAdminMock.mockResolvedValue(true); + }); + afterEach(() => { + [getUserMock, setBannedMock, georgeUserBannedMock, isAdminMock].forEach( + (it) => it.mockReset() + ); + }); + + it("bans user with discord", async () => { + //GIVEN + const uid = "myUid"; + const user = { + uid, + name: "name", + email: "email", + discordId: "discordId", + } as unknown as MonkeyTypes.DBUser; + getUserMock.mockResolvedValue(user); + + //WHEN + await mockApp + .post("/admin/toggleBan") + .set("Authorization", "Bearer 123456789") + .send({ uid }) + .set({ + Accept: "application/json", + }) + .expect(200); + + //THEN + expect(getUserMock).toHaveBeenLastCalledWith(uid, "toggle ban"); + expect(setBannedMock).toHaveBeenCalledWith(uid, true); + expect(georgeUserBannedMock).toHaveBeenCalledWith("discordId", true); + }); + it("bans user without discord", async () => { + //GIVEN + const uid = "myUid"; + const user = { + uid, + name: "name", + email: "email", + discordId: "", + } as unknown as MonkeyTypes.DBUser; + getUserMock.mockResolvedValue(user); + + //WHEN + await mockApp + .post("/admin/toggleBan") + .set("Authorization", "Bearer 123456789") + .send({ uid }) + .set({ + Accept: "application/json", + }) + .expect(200); + + //THEN + expect(georgeUserBannedMock).not.toHaveBeenCalled(); + }); + it("unbans user with discord", async () => { + //GIVEN + const uid = "myUid"; + + const user = { + uid, + name: "name", + email: "email", + discordId: "discordId", + banned: true, + } as unknown as MonkeyTypes.DBUser; + getUserMock.mockResolvedValue(user); + + //WHEN + await mockApp + .post("/admin/toggleBan") + .set("Authorization", "Bearer 123456789") + .send({ uid }) + .set({ + Accept: "application/json", + }) + .expect(200); + + //THEN + expect(getUserMock).toHaveBeenLastCalledWith(uid, "toggle ban"); + expect(setBannedMock).toHaveBeenCalledWith(uid, false); + expect(georgeUserBannedMock).toHaveBeenCalledWith("discordId", false); + }); + it("unbans user without discord", async () => { + //GIVEN + const uid = "myUid"; + + const user = { + uid, + name: "name", + email: "email", + discordId: "", + banned: true, + } as unknown as MonkeyTypes.DBUser; + getUserMock.mockResolvedValue(user); + + //WHEN + await mockApp + .post("/admin/toggleBan") + .set("Authorization", "Bearer 123456789") + .send({ uid }) + .set({ + Accept: "application/json", + }) + .expect(200); + + //THEN + expect(georgeUserBannedMock).not.toHaveBeenCalled(); + }); + }); + + describe("delete user", () => { + const getUserMock = vi.spyOn(UserDal, "getUser"); + const deleteUserMock = vi.spyOn(UserDal, "deleteUser"); + const firebaseDeleteUserMock = vi.spyOn(AuthUtils, "deleteUser"); + const deleteAllApeKeysMock = vi.spyOn(ApeKeys, "deleteAllApeKeys"); + const deleteAllPresetsMock = vi.spyOn(PresetDal, "deleteAllPresets"); + const deleteConfigMock = vi.spyOn(ConfigDal, "deleteConfig"); + const deleteAllResultMock = vi.spyOn(ResultDal, "deleteAll"); + const purgeUserFromDailyLeaderboardsMock = vi.spyOn( + DailyLeaderboards, + "purgeUserFromDailyLeaderboards" + ); + const blocklistAddMock = vi.spyOn(BlocklistDal, "add"); + + beforeEach(() => { + [ + firebaseDeleteUserMock, + deleteUserMock, + blocklistAddMock, + deleteAllApeKeysMock, + deleteAllPresetsMock, + deleteConfigMock, + purgeUserFromDailyLeaderboardsMock, + ].forEach((it) => it.mockResolvedValue(undefined)); + + deleteAllResultMock.mockResolvedValue({} as any); + }); + + afterEach(() => { + [ + getUserMock, + deleteUserMock, + blocklistAddMock, + firebaseDeleteUserMock, + deleteConfigMock, + deleteAllResultMock, + deleteAllApeKeysMock, + deleteAllPresetsMock, + purgeUserFromDailyLeaderboardsMock, + ].forEach((it) => it.mockReset()); + }); + + it("should add user to blocklist if banned", async () => { + //GIVEN + const uid = mockDecodedToken.uid; + const user = { + uid, + name: "name", + email: "email", + discordId: "discordId", + banned: true, + } as unknown as MonkeyTypes.DBUser; + await getUserMock.mockResolvedValue(user); + + //WHEN + await mockApp + .delete("/users/") + .set("Authorization", "Bearer 123456789") + .set({ + Accept: "application/json", + }) + .expect(200); + + //THEN + expect(blocklistAddMock).toHaveBeenCalledWith(user); + + expect(deleteUserMock).toHaveBeenCalledWith(uid); + expect(firebaseDeleteUserMock).toHaveBeenCalledWith(uid); + expect(deleteAllApeKeysMock).toHaveBeenCalledWith(uid); + expect(deleteAllPresetsMock).toHaveBeenCalledWith(uid); + expect(deleteConfigMock).toHaveBeenCalledWith(uid); + expect(deleteAllResultMock).toHaveBeenCalledWith(uid); + expect(purgeUserFromDailyLeaderboardsMock).toHaveBeenCalledWith( + uid, + (await configuration).dailyLeaderboards + ); + }); + it("should delete user without adding to blocklist if not banned", async () => { + //GIVEN + const uid = mockDecodedToken.uid; + const user = { + uid, + name: "name", + email: "email", + discordId: "discordId", + } as unknown as MonkeyTypes.DBUser; + getUserMock.mockResolvedValue(user); + + //WHEN + await mockApp + .delete("/users/") + .set("Authorization", "Bearer 123456789") + .set({ + Accept: "application/json", + }) + .expect(200); + + //THEN + expect(blocklistAddMock).not.toHaveBeenCalled(); + + expect(deleteUserMock).toHaveBeenCalledWith(uid); + expect(firebaseDeleteUserMock).toHaveBeenCalledWith(uid); + expect(deleteAllApeKeysMock).toHaveBeenCalledWith(uid); + expect(deleteAllPresetsMock).toHaveBeenCalledWith(uid); + expect(deleteConfigMock).toHaveBeenCalledWith(uid); + expect(deleteAllResultMock).toHaveBeenCalledWith(uid); + expect(purgeUserFromDailyLeaderboardsMock).toHaveBeenCalledWith( + uid, + (await configuration).dailyLeaderboards + ); + }); + }); + describe("link discord", () => { + const getUserMock = vi.spyOn(UserDal, "getUser"); + const isDiscordIdAvailableMock = vi.spyOn(UserDal, "isDiscordIdAvailable"); + const isStateValidForUserMock = vi.spyOn( + DiscordUtils, + "iStateValidForUser" + ); + const getDiscordUserMock = vi.spyOn(DiscordUtils, "getDiscordUser"); + const blocklistContainsMock = vi.spyOn(BlocklistDal, "contains"); + + beforeEach(async () => { + isStateValidForUserMock.mockResolvedValue(true); + getDiscordUserMock.mockResolvedValue({ + id: "discordUserId", + avatar: "discorUserAvatar", + username: "discordUserName", + discriminator: "discordUserDiscriminator", + }); + isDiscordIdAvailableMock.mockResolvedValue(true); + blocklistContainsMock.mockResolvedValue(false); + await enableDiscordIntegration(true); + }); + afterEach(() => { + [ + getUserMock, + isStateValidForUserMock, + isDiscordIdAvailableMock, + getDiscordUserMock, + ].forEach((it) => it.mockReset()); + }); + + it("should not link if discordId is blocked", async () => { + //GIVEN + const uid = mockDecodedToken.uid; + const user = { + uid, + name: "name", + email: "email", + } as unknown as MonkeyTypes.DBUser; + getUserMock.mockResolvedValue(user); + blocklistContainsMock.mockResolvedValue(true); + + //WHEN + const result = await mockApp + .post("/users/discord/link") + .set("Authorization", "Bearer 123456789") + .set({ + Accept: "application/json", + }) + .send({ + tokenType: "tokenType", + accessToken: "accessToken", + state: "statestatestatestate", + }) + .expect(409); + + //THEN + expect(result.body.message).toEqual("The Discord account is blocked"); + + expect(blocklistContainsMock).toBeCalledWith({ + discordId: "discordUserId", + }); + }); + }); }); function fillYearWithDay(days: number): number[] { @@ -210,8 +610,6 @@ function fillYearWithDay(days: number): number[] { return result; } -const configuration = Configuration.getCachedConfiguration(); - async function enablePremiumFeatures(premium: boolean): Promise { const mockConfig = _.merge(await configuration, { users: { premium: { enabled: premium } }, @@ -221,3 +619,33 @@ async function enablePremiumFeatures(premium: boolean): Promise { mockConfig ); } + +async function enableAdminFeatures(enabled: boolean): Promise { + const mockConfig = _.merge(await configuration, { + admin: { endpointsEnabled: enabled }, + }); + + vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue( + mockConfig + ); +} + +async function enableSignup(enabled: boolean): Promise { + const mockConfig = _.merge(await configuration, { + users: { signUp: enabled }, + }); + + vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue( + mockConfig + ); +} + +async function enableDiscordIntegration(enabled: boolean): Promise { + const mockConfig = _.merge(await configuration, { + users: { discordIntegration: { enabled } }, + }); + + vi.spyOn(Configuration, "getCachedConfiguration").mockResolvedValue( + mockConfig + ); +} diff --git a/backend/__tests__/dal/blocklist.spec.ts b/backend/__tests__/dal/blocklist.spec.ts new file mode 100644 index 000000000..44deb3eed --- /dev/null +++ b/backend/__tests__/dal/blocklist.spec.ts @@ -0,0 +1,339 @@ +import { ObjectId } from "mongodb"; +import * as BlacklistDal from "../../src/dal/blocklist"; + +describe("BlocklistDal", () => { + describe("add", () => { + beforeEach(() => { + vitest.useFakeTimers(); + }); + afterEach(() => { + vitest.useRealTimers(); + }); + it("adds user", async () => { + //GIVEN + const now = 1715082588; + vitest.setSystemTime(now); + + const name = "user" + new ObjectId().toHexString(); + const email = `${name}@example.com`; + + //WHEN + await BlacklistDal.add({ name, email }); + + //THEN + expect( + BlacklistDal.getCollection().findOne({ + emailHash: BlacklistDal.hash(email), + }) + ).resolves.toMatchObject({ + emailHash: BlacklistDal.hash(email), + timestamp: now, + }); + + expect( + BlacklistDal.getCollection().findOne({ + usernameHash: BlacklistDal.hash(name), + }) + ).resolves.toMatchObject({ + usernameHash: BlacklistDal.hash(name), + timestamp: now, + }); + }); + it("adds user with discordId", async () => { + //GIVEN + const now = 1715082588; + vitest.setSystemTime(now); + + const name = "user" + new ObjectId().toHexString(); + const email = `${name}@example.com`; + const discordId = `${name}DiscordId`; + + //WHEN + await BlacklistDal.add({ name, email, discordId }); + + //THEN + expect( + BlacklistDal.getCollection().findOne({ + discordIdHash: BlacklistDal.hash(discordId), + }) + ).resolves.toMatchObject({ + discordIdHash: BlacklistDal.hash(discordId), + timestamp: now, + }); + }); + it("adds user should not create duplicate name", async () => { + //GIVEN + const now = 1715082588; + vitest.setSystemTime(now); + + const name = "user" + new ObjectId().toHexString(); + const email = `${name}@example.com`; + const email2 = `${name}@otherdomain.com`; + await BlacklistDal.add({ name, email }); + + //WHEN + await BlacklistDal.add({ name, email: email2 }); + + //THEN + expect( + BlacklistDal.getCollection() + .find({ + usernameHash: BlacklistDal.hash(name), + }) + .toArray() + ).resolves.toHaveLength(1); + expect( + BlacklistDal.getCollection() + .find({ + emailHash: BlacklistDal.hash(email), + }) + .toArray() + ).resolves.toHaveLength(1); + expect( + BlacklistDal.getCollection() + .find({ + emailHash: BlacklistDal.hash(email2), + }) + .toArray() + ).resolves.toHaveLength(1); + }); + it("adds user should not create duplicate email", async () => { + //GIVEN + const now = 1715082588; + vitest.setSystemTime(now); + + const name = "user" + new ObjectId().toHexString(); + const email = `${name}@example.com`; + const name2 = "user" + new ObjectId().toHexString(); + await BlacklistDal.add({ name, email }); + + //WHEN + await BlacklistDal.add({ name: name2, email }); + + //THEN + expect( + BlacklistDal.getCollection() + .find({ + emailHash: BlacklistDal.hash(email), + }) + .toArray() + ).resolves.toHaveLength(1); + }); + it("adds user should not create duplicate discordId", async () => { + //GIVEN + const now = 1715082588; + vitest.setSystemTime(now); + + const name = "user" + new ObjectId().toHexString(); + const name2 = "user" + new ObjectId().toHexString(); + const email = `${name}@example.com`; + const discordId = `${name}DiscordId`; + + await BlacklistDal.add({ name, email, discordId }); + + //WHEN + await BlacklistDal.add({ name: name2, email, discordId }); + + //THEN + + expect( + BlacklistDal.getCollection() + .find({ + discordIdHash: BlacklistDal.hash(discordId), + }) + .toArray() + ).resolves.toHaveLength(1); + }); + }); + describe("contains", () => { + it("contains user", async () => { + //GIVEN + const name = "user" + new ObjectId().toHexString(); + const email = `${name}@example.com`; + const discordId = `${name}DiscordId`; + await BlacklistDal.add({ name, email, discordId }); + await BlacklistDal.add({ name: "test", email: "test@example.com" }); + + //WHEN / THEN + //by name + expect(BlacklistDal.contains({ name })).resolves.toBeTruthy(); + expect( + BlacklistDal.contains({ name: name.toUpperCase() }) + ).resolves.toBeTruthy(); + expect( + BlacklistDal.contains({ name, email: "unknown", discordId: "unknown" }) + ).resolves.toBeTruthy(); + + //by email + expect(BlacklistDal.contains({ email })).resolves.toBeTruthy(); + expect( + BlacklistDal.contains({ email: email.toUpperCase() }) + ).resolves.toBeTruthy(); + expect( + BlacklistDal.contains({ name: "unknown", email, discordId: "unknown" }) + ).resolves.toBeTruthy(); + + //by discordId + expect(BlacklistDal.contains({ discordId })).resolves.toBeTruthy(); + expect( + BlacklistDal.contains({ discordId: discordId.toUpperCase() }) + ).resolves.toBeTruthy(); + expect( + BlacklistDal.contains({ name: "unknown", email: "unknown", discordId }) + ).resolves.toBeTruthy(); + + //by name and email and discordId + expect( + BlacklistDal.contains({ name, email, discordId }) + ).resolves.toBeTruthy(); + }); + it("does not contain user", async () => { + //GIVEN + await BlacklistDal.add({ name: "test", email: "test@example.com" }); + await BlacklistDal.add({ name: "test2", email: "test2@example.com" }); + + //WHEN / THEN + expect(BlacklistDal.contains({ name: "unknown" })).resolves.toBeFalsy(); + expect(BlacklistDal.contains({ email: "unknown" })).resolves.toBeFalsy(); + expect( + BlacklistDal.contains({ discordId: "unknown" }) + ).resolves.toBeFalsy(); + expect( + BlacklistDal.contains({ + name: "unknown", + email: "unknown", + discordId: "unknown", + }) + ).resolves.toBeFalsy(); + + expect(BlacklistDal.contains({})).resolves.toBeFalsy(); + }); + }); + + describe("remove", () => { + it("removes existing username", async () => { + //GIVEN + const name = "user" + new ObjectId().toHexString(); + const email = `${name}@example.com`; + await BlacklistDal.add({ name, email }); + await BlacklistDal.add({ name: "test", email: "test@example.com" }); + + //WHEN + await BlacklistDal.remove({ name }); + + //THEN + expect(BlacklistDal.contains({ name })).resolves.toBeFalsy(); + expect(BlacklistDal.contains({ email })).resolves.toBeTruthy(); + + //decoy still exists + expect(BlacklistDal.contains({ name: "test" })).resolves.toBeTruthy(); + expect( + BlacklistDal.contains({ email: "test@example.com" }) + ).resolves.toBeTruthy(); + }); + it("removes existing email", async () => { + //GIVEN + const name = "user" + new ObjectId().toHexString(); + const email = `${name}@example.com`; + await BlacklistDal.add({ name, email }); + await BlacklistDal.add({ name: "test", email: "test@example.com" }); + + //WHEN + await BlacklistDal.remove({ email }); + + //THEN + expect(BlacklistDal.contains({ email })).resolves.toBeFalsy(); + expect(BlacklistDal.contains({ name })).resolves.toBeTruthy(); + + //decoy still exists + expect(BlacklistDal.contains({ name: "test" })).resolves.toBeTruthy(); + expect( + BlacklistDal.contains({ email: "test@example.com" }) + ).resolves.toBeTruthy(); + }); + it("removes existing discordId", async () => { + //GIVEN + const name = "user" + new ObjectId().toHexString(); + const email = `${name}@example.com`; + const discordId = `${name}DiscordId`; + await BlacklistDal.add({ name, email, discordId }); + await BlacklistDal.add({ + name: "test", + email: "test@example.com", + discordId: "testDiscordId", + }); + + //WHEN + await BlacklistDal.remove({ discordId }); + + //THEN + expect(BlacklistDal.contains({ discordId })).resolves.toBeFalsy(); + expect(BlacklistDal.contains({ name })).resolves.toBeTruthy(); + expect(BlacklistDal.contains({ email })).resolves.toBeTruthy(); + + //decoy still exists + expect(BlacklistDal.contains({ name: "test" })).resolves.toBeTruthy(); + expect( + BlacklistDal.contains({ email: "test@example.com" }) + ).resolves.toBeTruthy(); + expect( + BlacklistDal.contains({ discordId: "testDiscordId" }) + ).resolves.toBeTruthy(); + }); + it("removes existing username,email and discordId", async () => { + //GIVEN + const name = "user" + new ObjectId().toHexString(); + const email = `${name}@example.com`; + const discordId = `${name}DiscordId`; + await BlacklistDal.add({ name, email, discordId }); + await BlacklistDal.add({ + name: "test", + email: "test@example.com", + discordId: "testDiscordId", + }); + + //WHEN + await BlacklistDal.remove({ name, email, discordId }); + + //THEN + expect(BlacklistDal.contains({ email })).resolves.toBeFalsy(); + expect(BlacklistDal.contains({ name })).resolves.toBeFalsy(); + expect(BlacklistDal.contains({ discordId })).resolves.toBeFalsy(); + + //decoy still exists + expect(BlacklistDal.contains({ name: "test" })).resolves.toBeTruthy(); + expect( + BlacklistDal.contains({ email: "test@example.com" }) + ).resolves.toBeTruthy(); + expect( + BlacklistDal.contains({ discordId: "testDiscordId" }) + ).resolves.toBeTruthy(); + }); + + it("does not remove for empty user", async () => { + //GIVEN + const name = "user" + new ObjectId().toHexString(); + const email = `${name}@example.com`; + const discordId = `${name}DiscordId`; + await BlacklistDal.add({ name, email, discordId }); + await BlacklistDal.add({ name: "test", email: "test@example.com" }); + + //WHEN + await BlacklistDal.remove({}); + + //THEN + expect(BlacklistDal.contains({ email })).resolves.toBeTruthy(); + expect(BlacklistDal.contains({ name })).resolves.toBeTruthy(); + expect(BlacklistDal.contains({ discordId })).resolves.toBeTruthy(); + }); + }); + describe("hash", () => { + it("hashes case insensitive", () => { + ["test", "TEST", "tESt"].forEach((value) => + expect(BlacklistDal.hash(value)).toEqual( + "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" + ) + ); + }); + }); +}); diff --git a/backend/__tests__/global-setup.ts b/backend/__tests__/global-setup.ts new file mode 100644 index 000000000..461ccb2f4 --- /dev/null +++ b/backend/__tests__/global-setup.ts @@ -0,0 +1,14 @@ +import * as MongoDbMock from "vitest-mongodb"; +export async function setup({ provide }): Promise { + await MongoDbMock.setup({ + serverOptions: { + binary: { + version: "6.0.12", + }, + }, + }); +} + +export async function teardown(): Promise { + await MongoDbMock.teardown(); +} diff --git a/backend/__tests__/setup-tests.ts b/backend/__tests__/setup-tests.ts index ccf048ef9..5d267700c 100644 --- a/backend/__tests__/setup-tests.ts +++ b/backend/__tests__/setup-tests.ts @@ -1,78 +1,77 @@ import { Collection, Db, MongoClient, WithId } from "mongodb"; -import { afterAll, beforeAll, beforeEach, afterEach } from "vitest"; +import { afterAll, beforeAll, afterEach } from "vitest"; import * as MongoDbMock from "vitest-mongodb"; process.env["MODE"] = "dev"; - -vi.mock("../src/init/db", () => ({ - __esModule: true, - getDb: (): Db => db, - collection: (name: string): Collection> => - db.collection>(name), - close: () => client?.close(), -})); - -vi.mock("../src/utils/logger", () => ({ - __esModule: true, - default: { - error: console.error, - warning: console.warn, - info: console.info, - success: console.info, - logToDb: console.info, - }, -})); - -vi.mock("swagger-stats", () => ({ - getMiddleware: - () => - (_: unknown, __: unknown, next: () => unknown): void => { - next(); - }, -})); +//process.env["MONGOMS_DISTRO"] = "ubuntu-22.04"; if (!process.env["REDIS_URI"]) { // use mock if not set process.env["REDIS_URI"] = "redis://mock"; } -// TODO: better approach for this when needed -// https://firebase.google.com/docs/rules/unit-tests#run_local_unit_tests_with_the_version_9_javascript_sdk -vi.mock("firebase-admin", () => ({ - __esModule: true, - default: { - auth: (): unknown => ({ - verifyIdToken: ( - _token: string, - _checkRevoked: boolean - ): unknown /* Promise */ => - Promise.resolve({ - aud: "mockFirebaseProjectId", - auth_time: 123, - exp: 1000, - uid: "mockUid", - }), - }), - }, -})); - -const collectionsForCleanUp = ["users"]; - let db: Db; let client: MongoClient; +const collectionsForCleanUp = ["users"]; + beforeAll(async () => { await MongoDbMock.setup({ - serverOptions: { - binary: { - version: "6.0.12", - }, - }, + //don't add any configuration here, add to global-setup.ts instead. }); + client = new MongoClient(globalThis.__MONGO_URI__); + await client.connect(); db = client.db(); + + vi.mock("../src/init/db", () => ({ + __esModule: true, + getDb: (): Db => db, + collection: (name: string): Collection> => + db.collection>(name), + close: () => {}, + })); + + vi.mock("../src/utils/logger", () => ({ + __esModule: true, + default: { + error: console.error, + warning: console.warn, + info: console.info, + success: console.info, + logToDb: console.info, + }, + })); + + vi.mock("swagger-stats", () => ({ + getMiddleware: + () => + (_: unknown, __: unknown, next: () => unknown): void => { + next(); + }, + })); + + // TODO: better approach for this when needed + // https://firebase.google.com/docs/rules/unit-tests#run_local_unit_tests_with_the_version_9_javascript_sdk + vi.mock("firebase-admin", () => ({ + __esModule: true, + default: { + auth: (): unknown => ({ + verifyIdToken: ( + _token: string, + _checkRevoked: boolean + ): unknown /* Promise */ => + Promise.resolve({ + aud: "mockFirebaseProjectId", + auth_time: 123, + exp: 1000, + uid: "mockUid", + }), + }), + }, + })); }); -beforeEach(async () => { +afterEach(async () => { if (globalThis.__MONGO_URI__) { await Promise.all( collectionsForCleanUp.map((collection) => @@ -82,13 +81,12 @@ beforeEach(async () => { } }); -const realDateNow = Date.now; - -afterEach(() => { - Date.now = realDateNow; -}); - afterAll(async () => { await client?.close(); await MongoDbMock.teardown(); + // @ts-ignore + db = undefined; + //@ts-ignore + client = undefined; + vi.resetAllMocks(); }); diff --git a/backend/__tests__/utils/misc.spec.ts b/backend/__tests__/utils/misc.spec.ts index a02995af2..f8c2b0065 100644 --- a/backend/__tests__/utils/misc.spec.ts +++ b/backend/__tests__/utils/misc.spec.ts @@ -2,8 +2,13 @@ import _ from "lodash"; import * as misc from "../../src/utils/misc"; describe("Misc Utils", () => { + afterAll(() => { + vi.useRealTimers(); + }); + it("getCurrentDayTimestamp", () => { - Date.now = vi.fn(() => 1652743381); + vi.useFakeTimers(); + vi.setSystemTime(1652743381); const currentDay = misc.getCurrentDayTimestamp(); expect(currentDay).toBe(1641600000); diff --git a/backend/package-lock.json b/backend/package-lock.json index c214593d8..45b9c3e81 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -62,13 +62,13 @@ "@types/swagger-ui-express": "4.1.3", "@types/ua-parser-js": "0.7.36", "@types/uuid": "8.3.4", - "@vitest/coverage-v8": "^1.6.0", + "@vitest/coverage-v8": "1.6.0", "ioredis-mock": "7.4.0", "readline-sync": "1.4.10", "supertest": "6.2.3", "ts-node-dev": "2.0.0", "typescript": "5.3.3", - "vitest": "^1.6.0", + "vitest": "1.6.0", "vitest-mongodb": "0.0.5" }, "engines": { @@ -104,6 +104,7 @@ "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz", "integrity": "sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==", "optional": true, + "peer": true, "dependencies": { "@aws-crypto/util": "^3.0.0", "@aws-sdk/types": "^3.222.0", @@ -114,13 +115,15 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "optional": true + "optional": true, + "peer": true }, "node_modules/@aws-crypto/ie11-detection": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-3.0.0.tgz", "integrity": "sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==", "optional": true, + "peer": true, "dependencies": { "tslib": "^1.11.1" } @@ -129,13 +132,15 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "optional": true + "optional": true, + "peer": true }, "node_modules/@aws-crypto/sha256-browser": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-3.0.0.tgz", "integrity": "sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==", "optional": true, + "peer": true, "dependencies": { "@aws-crypto/ie11-detection": "^3.0.0", "@aws-crypto/sha256-js": "^3.0.0", @@ -151,13 +156,15 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "optional": true + "optional": true, + "peer": true }, "node_modules/@aws-crypto/sha256-js": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-3.0.0.tgz", "integrity": "sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==", "optional": true, + "peer": true, "dependencies": { "@aws-crypto/util": "^3.0.0", "@aws-sdk/types": "^3.222.0", @@ -168,13 +175,15 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "optional": true + "optional": true, + "peer": true }, "node_modules/@aws-crypto/supports-web-crypto": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-3.0.0.tgz", "integrity": "sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==", "optional": true, + "peer": true, "dependencies": { "tslib": "^1.11.1" } @@ -183,13 +192,15 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "optional": true + "optional": true, + "peer": true }, "node_modules/@aws-crypto/util": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-3.0.0.tgz", "integrity": "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==", "optional": true, + "peer": true, "dependencies": { "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-utf8-browser": "^3.0.0", @@ -200,13 +211,15 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "optional": true + "optional": true, + "peer": true }, "node_modules/@aws-sdk/client-cognito-identity": { "version": "3.451.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.451.0.tgz", "integrity": "sha512-xoImUiGoaXJZpOCgbWcdrU4vHJ8HG5KluaCkc32kuFobM277sjQimaUIHOGHL24M5vyo4QxcJD9CT/IhX63Vlg==", "optional": true, + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", @@ -257,6 +270,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.451.0.tgz", "integrity": "sha512-KkYSke3Pdv3MfVH/5fT528+MKjMyPKlcLcd4zQb0x6/7Bl7EHrPh1JZYjzPLHelb+UY5X0qN8+cb8iSu1eiwIQ==", "optional": true, + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", @@ -304,6 +318,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.451.0.tgz", "integrity": "sha512-48NcIRxWBdP1fom6RSjwn2R2u7SE7eeV3p+c4s7ukEOfrHhBxJfn3EpqBVQMGzdiU55qFImy+Fe81iA2lXq3Jw==", "optional": true, + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", @@ -355,6 +370,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.451.0.tgz", "integrity": "sha512-SamWW2zHEf1ZKe3j1w0Piauryl8BQIlej0TBS18A4ACzhjhWXhCs13bO1S88LvPR5mBFXok3XOT6zPOnKDFktw==", "optional": true, + "peer": true, "dependencies": { "@smithy/smithy-client": "^2.1.15", "tslib": "^2.5.0" @@ -368,6 +384,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.451.0.tgz", "integrity": "sha512-g1ZT46NuYfou00d94rJZ59N4TLI1T+v46lbHTtF9jwohiUsi7/vHkPIOdrgtrThGzGUVl01w62N0a2mpMydaBA==", "optional": true, + "peer": true, "dependencies": { "@aws-sdk/client-cognito-identity": "3.451.0", "@aws-sdk/types": "3.451.0", @@ -384,6 +401,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.451.0.tgz", "integrity": "sha512-9dAav7DcRgaF7xCJEQR5ER9ErXxnu/tdnVJ+UPmb1NPeIZdESv1A3lxFDEq1Fs8c4/lzAj9BpshGyJVIZwZDKg==", "optional": true, + "peer": true, "dependencies": { "@aws-sdk/types": "3.451.0", "@smithy/property-provider": "^2.0.0", @@ -399,6 +417,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.451.0.tgz", "integrity": "sha512-q82kEzymqimkJ2dHmuN2RGpi9HTFSxwwoXALnzPRaRcvR/v+YY8FMgSTfwXzPkHUDf/q8J+aDz6lPcYlnsP3sQ==", "optional": true, + "peer": true, "dependencies": { "@aws-sdk/types": "3.451.0", "@smithy/fetch-http-handler": "^2.2.6", @@ -419,6 +438,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.451.0.tgz", "integrity": "sha512-TySt64Ci5/ZbqFw1F9Z0FIGvYx5JSC9e6gqDnizIYd8eMnn8wFRUscRrD7pIHKfrhvVKN5h0GdYovmMO/FMCBw==", "optional": true, + "peer": true, "dependencies": { "@aws-sdk/credential-provider-env": "3.451.0", "@aws-sdk/credential-provider-process": "3.451.0", @@ -440,6 +460,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.451.0.tgz", "integrity": "sha512-AEwM1WPyxUdKrKyUsKyFqqRFGU70e4qlDyrtBxJnSU9NRLZI8tfEZ67bN7fHSxBUBODgDXpMSlSvJiBLh5/3pw==", "optional": true, + "peer": true, "dependencies": { "@aws-sdk/credential-provider-env": "3.451.0", "@aws-sdk/credential-provider-ini": "3.451.0", @@ -462,6 +483,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.451.0.tgz", "integrity": "sha512-HQywSdKeD5PErcLLnZfSyCJO+6T+ZyzF+Lm/QgscSC+CbSUSIPi//s15qhBRVely/3KBV6AywxwNH+5eYgt4lQ==", "optional": true, + "peer": true, "dependencies": { "@aws-sdk/types": "3.451.0", "@smithy/property-provider": "^2.0.0", @@ -478,6 +500,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.451.0.tgz", "integrity": "sha512-Usm/N51+unOt8ID4HnQzxIjUJDrkAQ1vyTOC0gSEEJ7h64NSSPGD5yhN7il5WcErtRd3EEtT1a8/GTC5TdBctg==", "optional": true, + "peer": true, "dependencies": { "@aws-sdk/client-sso": "3.451.0", "@aws-sdk/token-providers": "3.451.0", @@ -496,6 +519,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.451.0.tgz", "integrity": "sha512-Xtg3Qw65EfDjWNG7o2xD6sEmumPfsy3WDGjk2phEzVg8s7hcZGxf5wYwe6UY7RJvlEKrU0rFA+AMn6Hfj5oOzg==", "optional": true, + "peer": true, "dependencies": { "@aws-sdk/types": "3.451.0", "@smithy/property-provider": "^2.0.0", @@ -511,6 +535,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.451.0.tgz", "integrity": "sha512-ihbYZrI/tSVsZFDGLfJoCx3sg1s9EQqWA+xbLoquK+RjMqTnaeshYntFJmQA5yqCIbcAkyw63OwOIBRrVb7tMA==", "optional": true, + "peer": true, "dependencies": { "@aws-sdk/client-cognito-identity": "3.451.0", "@aws-sdk/client-sso": "3.451.0", @@ -538,6 +563,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.451.0.tgz", "integrity": "sha512-j8a5jAfhWmsK99i2k8oR8zzQgXrsJtgrLxc3js6U+525mcZytoiDndkWTmD5fjJ1byU1U2E5TaPq+QJeDip05Q==", "optional": true, + "peer": true, "dependencies": { "@aws-sdk/types": "3.451.0", "@smithy/protocol-http": "^3.0.9", @@ -553,6 +579,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.451.0.tgz", "integrity": "sha512-0kHrYEyVeB2QBfP6TfbI240aRtatLZtcErJbhpiNUb+CQPgEL3crIjgVE8yYiJumZ7f0jyjo8HLPkwD1/2APaw==", "optional": true, + "peer": true, "dependencies": { "@aws-sdk/types": "3.451.0", "@smithy/types": "^2.5.0", @@ -567,6 +594,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.451.0.tgz", "integrity": "sha512-J6jL6gJ7orjHGM70KDRcCP7so/J2SnkN4vZ9YRLTeeZY6zvBuHDjX8GCIgSqPn/nXFXckZO8XSnA7u6+3TAT0w==", "optional": true, + "peer": true, "dependencies": { "@aws-sdk/types": "3.451.0", "@smithy/protocol-http": "^3.0.9", @@ -582,6 +610,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.451.0.tgz", "integrity": "sha512-UJ6UfVUEgp0KIztxpAeelPXI5MLj9wUtUCqYeIMP7C1ZhoEMNm3G39VLkGN43dNhBf1LqjsV9jkKMZbVfYXuwg==", "optional": true, + "peer": true, "dependencies": { "@aws-sdk/middleware-signing": "3.451.0", "@aws-sdk/types": "3.451.0", @@ -597,6 +626,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.451.0.tgz", "integrity": "sha512-s5ZlcIoLNg1Huj4Qp06iKniE8nJt/Pj1B/fjhWc6cCPCM7XJYUCejCnRh6C5ZJoBEYodjuwZBejPc1Wh3j+znA==", "optional": true, + "peer": true, "dependencies": { "@aws-sdk/types": "3.451.0", "@smithy/property-provider": "^2.0.0", @@ -615,6 +645,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.451.0.tgz", "integrity": "sha512-8NM/0JiKLNvT9wtAQVl1DFW0cEO7OvZyLSUBLNLTHqyvOZxKaZ8YFk7d8PL6l76LeUKRxq4NMxfZQlUIRe0eSA==", "optional": true, + "peer": true, "dependencies": { "@aws-sdk/types": "3.451.0", "@aws-sdk/util-endpoints": "3.451.0", @@ -631,6 +662,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.451.0.tgz", "integrity": "sha512-3iMf4OwzrFb4tAAmoROXaiORUk2FvSejnHIw/XHvf/jjR4EqGGF95NZP/n/MeFZMizJWVssrwS412GmoEyoqhg==", "optional": true, + "peer": true, "dependencies": { "@smithy/node-config-provider": "^2.1.5", "@smithy/types": "^2.5.0", @@ -647,6 +679,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.451.0.tgz", "integrity": "sha512-ij1L5iUbn6CwxVOT1PG4NFjsrsKN9c4N1YEM0lkl6DwmaNOscjLKGSNyj9M118vSWsOs1ZDbTwtj++h0O/BWrQ==", "optional": true, + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", @@ -695,6 +728,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.451.0.tgz", "integrity": "sha512-rhK+qeYwCIs+laJfWCcrYEjay2FR/9VABZJ2NRM89jV/fKqGVQR52E5DQqrI+oEIL5JHMhhnr4N4fyECMS35lw==", "optional": true, + "peer": true, "dependencies": { "@smithy/types": "^2.5.0", "tslib": "^2.5.0" @@ -708,6 +742,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.451.0.tgz", "integrity": "sha512-giqLGBTnRIcKkDqwU7+GQhKbtJ5Ku35cjGQIfMyOga6pwTBUbaK0xW1Sdd8sBQ1GhApscnChzI9o/R9x0368vw==", "optional": true, + "peer": true, "dependencies": { "@aws-sdk/types": "3.451.0", "@smithy/util-endpoints": "^1.0.4", @@ -722,6 +757,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.310.0.tgz", "integrity": "sha512-qo2t/vBTnoXpjKxlsC2e1gBrRm80M3bId27r0BRB2VniSSe7bL1mmzM+/HFtujm0iAxtPM+aLEflLJlJeDPg0w==", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.5.0" }, @@ -734,6 +770,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.451.0.tgz", "integrity": "sha512-Ws5mG3J0TQifH7OTcMrCTexo7HeSAc3cBgjfhS/ofzPUzVCtsyg0G7I6T7wl7vJJETix2Kst2cpOsxygPgPD9w==", "optional": true, + "peer": true, "dependencies": { "@aws-sdk/types": "3.451.0", "@smithy/types": "^2.5.0", @@ -746,6 +783,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.451.0.tgz", "integrity": "sha512-TBzm6P+ql4mkGFAjPlO1CI+w3yUT+NulaiALjl/jNX/nnUp6HsJsVxJf4nVFQTG5KRV0iqMypcs7I3KIhH+LmA==", "optional": true, + "peer": true, "dependencies": { "@aws-sdk/types": "3.451.0", "@smithy/node-config-provider": "^2.1.5", @@ -769,6 +807,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz", "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.3.1" } @@ -2127,6 +2166,7 @@ "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.0.13.tgz", "integrity": "sha512-eeOPD+GF9BzF/Mjy3PICLePx4l0f3rG/nQegQHRLTloN5p1lSJJNZsyn+FzDnW8P2AduragZqJdtKNCxXozB1Q==", "optional": true, + "peer": true, "dependencies": { "@smithy/types": "^2.5.0", "tslib": "^2.5.0" @@ -2140,6 +2180,7 @@ "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-2.0.18.tgz", "integrity": "sha512-761sJSgNbvsqcsKW6/WZbrZr4H+0Vp/QKKqwyrxCPwD8BsiPEXNHyYnqNgaeK9xRWYswjon0Uxbpe3DWQo0j/g==", "optional": true, + "peer": true, "dependencies": { "@smithy/node-config-provider": "^2.1.5", "@smithy/types": "^2.5.0", @@ -2156,6 +2197,7 @@ "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-2.1.1.tgz", "integrity": "sha512-gw5G3FjWC6sNz8zpOJgPpH5HGKrpoVFQpToNAwLwJVyI/LJ2jDJRjSKEsM6XI25aRpYjMSE/Qptxx305gN1vHw==", "optional": true, + "peer": true, "dependencies": { "@smithy/node-config-provider": "^2.1.5", "@smithy/property-provider": "^2.0.14", @@ -2172,6 +2214,7 @@ "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-2.0.13.tgz", "integrity": "sha512-CExbelIYp+DxAHG8RIs0l9QL7ElqhG4ym9BNoSpkPa4ptBQfzJdep3LbOSVJIE2VUdBAeObdeL6EDB3Jo85n3g==", "optional": true, + "peer": true, "dependencies": { "@aws-crypto/crc32": "3.0.0", "@smithy/types": "^2.5.0", @@ -2184,6 +2227,7 @@ "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-2.2.6.tgz", "integrity": "sha512-PStY3XO1Ksjwn3wMKye5U6m6zxXpXrXZYqLy/IeCbh3nM9QB3Jgw/B0PUSLUWKdXg4U8qgEu300e3ZoBvZLsDg==", "optional": true, + "peer": true, "dependencies": { "@smithy/protocol-http": "^3.0.9", "@smithy/querystring-builder": "^2.0.13", @@ -2197,6 +2241,7 @@ "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-2.0.15.tgz", "integrity": "sha512-t/qjEJZu/G46A22PAk1k/IiJZT4ncRkG5GOCNWN9HPPy5rCcSZUbh7gwp7CGKgJJ7ATMMg+0Td7i9o1lQTwOfQ==", "optional": true, + "peer": true, "dependencies": { "@smithy/types": "^2.5.0", "@smithy/util-buffer-from": "^2.0.0", @@ -2212,6 +2257,7 @@ "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-2.0.13.tgz", "integrity": "sha512-XsGYhVhvEikX1Yz0kyIoLssJf2Rs6E0U2w2YuKdT4jSra5A/g8V2oLROC1s56NldbgnpesTYB2z55KCHHbKyjw==", "optional": true, + "peer": true, "dependencies": { "@smithy/types": "^2.5.0", "tslib": "^2.5.0" @@ -2222,6 +2268,7 @@ "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.0.0.tgz", "integrity": "sha512-z3PjFjMyZNI98JFRJi/U0nGoLWMSJlDjAW4QUX2WNZLas5C0CmVV6LJ01JI0k90l7FvpmixjWxPFmENSClQ7ug==", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.5.0" }, @@ -2234,6 +2281,7 @@ "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-2.0.15.tgz", "integrity": "sha512-xH4kRBw01gJgWiU+/mNTrnyFXeozpZHw39gLb3JKGsFDVmSrJZ8/tRqu27tU/ki1gKkxr2wApu+dEYjI3QwV1Q==", "optional": true, + "peer": true, "dependencies": { "@smithy/protocol-http": "^3.0.9", "@smithy/types": "^2.5.0", @@ -2248,6 +2296,7 @@ "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-2.2.0.tgz", "integrity": "sha512-tddRmaig5URk2106PVMiNX6mc5BnKIKajHHDxb7K0J5MLdcuQluHMGnjkv18iY9s9O0tF+gAcPd/pDXA5L9DZw==", "optional": true, + "peer": true, "dependencies": { "@smithy/middleware-serde": "^2.0.13", "@smithy/node-config-provider": "^2.1.5", @@ -2266,6 +2315,7 @@ "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-2.0.20.tgz", "integrity": "sha512-X2yrF/SHDk2WDd8LflRNS955rlzQ9daz9UWSp15wW8KtzoTXg3bhHM78HbK1cjr48/FWERSJKh9AvRUUGlIawg==", "optional": true, + "peer": true, "dependencies": { "@smithy/node-config-provider": "^2.1.5", "@smithy/protocol-http": "^3.0.9", @@ -2285,6 +2335,7 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "optional": true, + "peer": true, "bin": { "uuid": "dist/bin/uuid" } @@ -2294,6 +2345,7 @@ "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-2.0.13.tgz", "integrity": "sha512-tBGbeXw+XsE6pPr4UaXOh+UIcXARZeiA8bKJWxk2IjJcD1icVLhBSUQH9myCIZLNNzJIH36SDjUX8Wqk4xJCJg==", "optional": true, + "peer": true, "dependencies": { "@smithy/types": "^2.5.0", "tslib": "^2.5.0" @@ -2307,6 +2359,7 @@ "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-2.0.7.tgz", "integrity": "sha512-L1KLAAWkXbGx1t2jjCI/mDJ2dDNq+rp4/ifr/HcC6FHngxho5O7A5bQLpKHGlkfATH6fUnOEx0VICEVFA4sUzw==", "optional": true, + "peer": true, "dependencies": { "@smithy/types": "^2.5.0", "tslib": "^2.5.0" @@ -2320,6 +2373,7 @@ "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-2.1.5.tgz", "integrity": "sha512-3Omb5/h4tOCuKRx4p4pkYTvEYRCYoKk52bOYbKUyz/G/8gERbagsN8jFm4FjQubkrcIqQEghTpQaUw6uk+0edw==", "optional": true, + "peer": true, "dependencies": { "@smithy/property-provider": "^2.0.14", "@smithy/shared-ini-file-loader": "^2.2.4", @@ -2335,6 +2389,7 @@ "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-2.1.9.tgz", "integrity": "sha512-+K0q3SlNcocmo9OZj+fz67gY4lwhOCvIJxVbo/xH+hfWObvaxrMTx7JEzzXcluK0thnnLz++K3Qe7Z/8MDUreA==", "optional": true, + "peer": true, "dependencies": { "@smithy/abort-controller": "^2.0.13", "@smithy/protocol-http": "^3.0.9", @@ -2351,6 +2406,7 @@ "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-2.0.14.tgz", "integrity": "sha512-k3D2qp9o6imTrLaXRj6GdLYEJr1sXqS99nLhzq8fYmJjSVOeMg/G+1KVAAc7Oxpu71rlZ2f8SSZxcSxkevuR0A==", "optional": true, + "peer": true, "dependencies": { "@smithy/types": "^2.5.0", "tslib": "^2.5.0" @@ -2364,6 +2420,7 @@ "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-3.0.9.tgz", "integrity": "sha512-U1wl+FhYu4/BC+rjwh1lg2gcJChQhytiNQSggREgQ9G2FzmoK9sACBZvx7thyWMvRyHQTE22mO2d5UM8gMKDBg==", "optional": true, + "peer": true, "dependencies": { "@smithy/types": "^2.5.0", "tslib": "^2.5.0" @@ -2377,6 +2434,7 @@ "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-2.0.13.tgz", "integrity": "sha512-JhXKwp3JtsFUe96XLHy/nUPEbaXqn6r7xE4sNaH8bxEyytE5q1fwt0ew/Ke6+vIC7gP87HCHgQpJHg1X1jN2Fw==", "optional": true, + "peer": true, "dependencies": { "@smithy/types": "^2.5.0", "@smithy/util-uri-escape": "^2.0.0", @@ -2391,6 +2449,7 @@ "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-2.0.13.tgz", "integrity": "sha512-TEiT6o8CPZVxJ44Rly/rrsATTQsE+b/nyBVzsYn2sa75xAaZcurNxsFd8z1haoUysONiyex24JMHoJY6iCfLdA==", "optional": true, + "peer": true, "dependencies": { "@smithy/types": "^2.5.0", "tslib": "^2.5.0" @@ -2404,6 +2463,7 @@ "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.0.6.tgz", "integrity": "sha512-fCQ36frtYra2fqY2/DV8+3/z2d0VB/1D1hXbjRcM5wkxTToxq6xHbIY/NGGY6v4carskMyG8FHACxgxturJ9Pg==", "optional": true, + "peer": true, "dependencies": { "@smithy/types": "^2.5.0" }, @@ -2416,6 +2476,7 @@ "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-2.2.4.tgz", "integrity": "sha512-9dRknGgvYlRIsoTcmMJXuoR/3ekhGwhRq4un3ns2/byre4Ql5hyUN4iS0x8eITohjU90YOnUCsbRwZRvCkbRfw==", "optional": true, + "peer": true, "dependencies": { "@smithy/types": "^2.5.0", "tslib": "^2.5.0" @@ -2429,6 +2490,7 @@ "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-2.0.15.tgz", "integrity": "sha512-SRTEJSEhQYVlBKIIdZ9SZpqW+KFqxqcNnEcBX+8xkDdWx+DItme9VcCDkdN32yTIrICC+irUufnUdV7mmHPjoA==", "optional": true, + "peer": true, "dependencies": { "@smithy/eventstream-codec": "^2.0.13", "@smithy/is-array-buffer": "^2.0.0", @@ -2448,6 +2510,7 @@ "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-2.1.15.tgz", "integrity": "sha512-rngZcQu7Jvs9UbHihK1EI67RMPuzkc3CJmu4MBgB7D7yBnMGuFR86tq5rqHfL2gAkNnMelBN/8kzQVvZjNKefQ==", "optional": true, + "peer": true, "dependencies": { "@smithy/middleware-stack": "^2.0.7", "@smithy/types": "^2.5.0", @@ -2463,6 +2526,7 @@ "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.5.0.tgz", "integrity": "sha512-/a31lYofrMBkJb3BuPlYJTMKDj0hUmKUP6JFZQu6YVuQVoAjubiY0A52U9S0Uysd33n/djexCUSNJ+G9bf3/aA==", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.5.0" }, @@ -2475,6 +2539,7 @@ "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-2.0.13.tgz", "integrity": "sha512-okWx2P/d9jcTsZWTVNnRMpFOE7fMkzloSFyM53fA7nLKJQObxM2T4JlZ5KitKKuXq7pxon9J6SF2kCwtdflIrA==", "optional": true, + "peer": true, "dependencies": { "@smithy/querystring-parser": "^2.0.13", "@smithy/types": "^2.5.0", @@ -2486,6 +2551,7 @@ "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-2.0.1.tgz", "integrity": "sha512-DlI6XFYDMsIVN+GH9JtcRp3j02JEVuWIn/QOZisVzpIAprdsxGveFed0bjbMRCqmIFe8uetn5rxzNrBtIGrPIQ==", "optional": true, + "peer": true, "dependencies": { "@smithy/util-buffer-from": "^2.0.0", "tslib": "^2.5.0" @@ -2499,6 +2565,7 @@ "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-2.0.0.tgz", "integrity": "sha512-JdDuS4ircJt+FDnaQj88TzZY3+njZ6O+D3uakS32f2VNnDo3vyEuNdBOh/oFd8Df1zSZOuH1HEChk2AOYDezZg==", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.5.0" } @@ -2508,6 +2575,7 @@ "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-2.1.0.tgz", "integrity": "sha512-/li0/kj/y3fQ3vyzn36NTLGmUwAICb7Jbe/CsWCktW363gh1MOcpEcSO3mJ344Gv2dqz8YJCLQpb6hju/0qOWw==", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.5.0" }, @@ -2520,6 +2588,7 @@ "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.0.0.tgz", "integrity": "sha512-/YNnLoHsR+4W4Vf2wL5lGv0ksg8Bmk3GEGxn2vEQt52AQaPSCuaO5PM5VM7lP1K9qHRKHwrPGktqVoAHKWHxzw==", "optional": true, + "peer": true, "dependencies": { "@smithy/is-array-buffer": "^2.0.0", "tslib": "^2.5.0" @@ -2533,6 +2602,7 @@ "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-2.0.0.tgz", "integrity": "sha512-xCQ6UapcIWKxXHEU4Mcs2s7LcFQRiU3XEluM2WcCjjBtQkUN71Tb+ydGmJFPxMUrW/GWMgQEEGipLym4XG0jZg==", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.5.0" }, @@ -2545,6 +2615,7 @@ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-2.0.19.tgz", "integrity": "sha512-VHP8xdFR7/orpiABJwgoTB0t8Zhhwpf93gXhNfUBiwAE9O0rvsv7LwpQYjgvbOUDDO8JfIYQB2GYJNkqqGWsXw==", "optional": true, + "peer": true, "dependencies": { "@smithy/property-provider": "^2.0.14", "@smithy/smithy-client": "^2.1.15", @@ -2561,6 +2632,7 @@ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.0.25.tgz", "integrity": "sha512-jkmep6/JyWmn2ADw9VULDeGbugR4N/FJCKOt+gYyVswmN1BJOfzF2umaYxQ1HhQDvna3kzm1Dbo1qIfBW4iuHA==", "optional": true, + "peer": true, "dependencies": { "@smithy/config-resolver": "^2.0.18", "@smithy/credential-provider-imds": "^2.1.1", @@ -2579,6 +2651,7 @@ "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-1.0.4.tgz", "integrity": "sha512-FPry8j1xye5yzrdnf4xKUXVnkQErxdN7bUIaqC0OFoGsv2NfD9b2UUMuZSSt+pr9a8XWAqj0HoyVNUfPiZ/PvQ==", "optional": true, + "peer": true, "dependencies": { "@smithy/node-config-provider": "^2.1.5", "@smithy/types": "^2.5.0", @@ -2593,6 +2666,7 @@ "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-2.0.0.tgz", "integrity": "sha512-c5xY+NUnFqG6d7HFh1IFfrm3mGl29lC+vF+geHv4ToiuJCBmIfzx6IeHLg+OgRdPFKDXIw6pvi+p3CsscaMcMA==", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.5.0" }, @@ -2605,6 +2679,7 @@ "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-2.0.6.tgz", "integrity": "sha512-7W4uuwBvSLgKoLC1x4LfeArCVcbuHdtVaC4g30kKsD1erfICyQ45+tFhhs/dZNeQg+w392fhunCm/+oCcb6BSA==", "optional": true, + "peer": true, "dependencies": { "@smithy/types": "^2.5.0", "tslib": "^2.5.0" @@ -2618,6 +2693,7 @@ "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-2.0.6.tgz", "integrity": "sha512-PSO41FofOBmyhPQJwBQJ6mVlaD7Sp9Uff9aBbnfBJ9eqXOE/obrqQjn0PNdkfdvViiPXl49BINfnGcFtSP4kYw==", "optional": true, + "peer": true, "dependencies": { "@smithy/service-error-classification": "^2.0.6", "@smithy/types": "^2.5.0", @@ -2632,6 +2708,7 @@ "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-2.0.20.tgz", "integrity": "sha512-tT8VASuD8jJu0yjHEMTCPt1o5E3FVzgdsxK6FQLAjXKqVv5V8InCnc0EOsYrijgspbfDqdAJg7r0o2sySfcHVg==", "optional": true, + "peer": true, "dependencies": { "@smithy/fetch-http-handler": "^2.2.6", "@smithy/node-http-handler": "^2.1.9", @@ -2651,6 +2728,7 @@ "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-2.0.0.tgz", "integrity": "sha512-ebkxsqinSdEooQduuk9CbKcI+wheijxEb3utGXkCoYQkJnwTnLbH1JXGimJtUkQwNQbsbuYwG2+aFVyZf5TLaw==", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.5.0" }, @@ -2663,6 +2741,7 @@ "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.0.2.tgz", "integrity": "sha512-qOiVORSPm6Ce4/Yu6hbSgNHABLP2VMv8QOC3tTDNHHlWY19pPyc++fBTbZPtx6egPXi4HQxKDnMxVxpbtX2GoA==", "optional": true, + "peer": true, "dependencies": { "@smithy/util-buffer-from": "^2.0.0", "tslib": "^2.5.0" @@ -3370,6 +3449,15 @@ "version": "3.2.3", "license": "MIT" }, + "node_modules/async-mutex": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", + "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==", + "dev": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/async-retry": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", @@ -3446,13 +3534,25 @@ "node": ">= 6" } }, + "node_modules/b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "dev": true + }, "node_modules/balanced-match": { "version": "1.0.2", "license": "MIT" }, + "node_modules/bare-events": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.2.tgz", + "integrity": "sha512-h7z00dWdG0PYOQEvChhOSWvOfkIKsdZGkWr083FgN/HyoQuebSew/cgirYqh9SCuy/hRvxc5Vy6Fw8xAmYHLkQ==", + "dev": true, + "optional": true + }, "node_modules/base64-js": { "version": "1.5.1", - "devOptional": true, "funding": [ { "type": "github", @@ -3467,7 +3567,8 @@ "url": "https://feross.org/support" } ], - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/basic-auth": { "version": "2.0.1", @@ -3511,23 +3612,6 @@ "node_modules/bintrees": { "version": "1.0.1" }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bl/node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, "node_modules/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz", @@ -3557,7 +3641,8 @@ "version": "2.11.0", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==", - "optional": true + "optional": true, + "peer": true }, "node_modules/brace-expansion": { "version": "1.1.11", @@ -3585,30 +3670,6 @@ "node": ">=16.20.1" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -4469,8 +4530,8 @@ }, "node_modules/end-of-stream": { "version": "1.4.4", - "devOptional": true, "license": "MIT", + "optional": true, "dependencies": { "once": "^1.4.0" } @@ -4662,6 +4723,12 @@ "devOptional": true, "license": "MIT" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "dev": true, @@ -4709,6 +4776,7 @@ } ], "optional": true, + "peer": true, "dependencies": { "strnum": "^1.0.5" }, @@ -4762,15 +4830,6 @@ "node": ">=0.8.0" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dev": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/fecha": { "version": "4.2.1", "license": "MIT" @@ -4922,9 +4981,9 @@ "license": "MIT" }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -5019,12 +5078,6 @@ "node": ">= 0.6" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true - }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -5255,18 +5308,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-port": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", - "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/glob": { "version": "7.2.0", "license": "ISC", @@ -5563,26 +5604,6 @@ "node": ">=0.10.0" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/ignore-by-default": { "version": "1.0.1", "license": "ISC" @@ -6369,18 +6390,6 @@ "dev": true, "license": "ISC" }, - "node_modules/md5-file": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-5.0.0.tgz", - "integrity": "sha512-xbEFXCYVWrSx/gEKS1VPlg84h/4L20znVIulKw6kMfmBUAZNAnF00eczz9ICMl+/hjQGo5KSXRxbL/47X3rmMw==", - "dev": true, - "bin": { - "md5-file": "cli.js" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -7046,6 +7055,195 @@ "node": ">=16" } }, + "node_modules/mongodb-memory-server": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-9.2.0.tgz", + "integrity": "sha512-w/usKdYtby5EALERxmA0+et+D0brP0InH3a26shNDgGefXA61hgl6U0P3IfwqZlEGRZdkbZig3n57AHZgDiwvg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "mongodb-memory-server-core": "9.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.20.1" + } + }, + "node_modules/mongodb-memory-server-core": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-9.2.0.tgz", + "integrity": "sha512-9SWZEy+dGj5Fvm5RY/mtqHZKS64o4heDwReD4SsfR7+uNgtYo+JN41kPCcJeIH3aJf04j25i5Dia2s52KmsMPA==", + "dev": true, + "dependencies": { + "async-mutex": "^0.4.0", + "camelcase": "^6.3.0", + "debug": "^4.3.4", + "find-cache-dir": "^3.3.2", + "follow-redirects": "^1.15.6", + "https-proxy-agent": "^7.0.4", + "mongodb": "^5.9.1", + "new-find-package-json": "^2.0.0", + "semver": "^7.6.0", + "tar-stream": "^3.1.7", + "tslib": "^2.6.2", + "yauzl": "^3.1.3" + }, + "engines": { + "node": ">=14.20.1" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/@types/whatwg-url": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", + "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/webidl-conversions": "*" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/bson": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-5.5.1.tgz", + "integrity": "sha512-ix0EwukN2EpC0SRWIj/7B5+A6uQMQy6KMREI9qQqvgpkV2frH63T0UDVd1SYedL6dNCmDBYB3QtXi4ISk9YT+g==", + "dev": true, + "engines": { + "node": ">=14.20.1" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mongodb-memory-server-core/node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/mongodb": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-5.9.2.tgz", + "integrity": "sha512-H60HecKO4Bc+7dhOv4sJlgvenK4fQNqqUIlXxZYQNbfEWSALGAwGoyJd/0Qwk4TttFXUOHJ2ZJQe/52ScaUwtQ==", + "dev": true, + "dependencies": { + "bson": "^5.5.0", + "mongodb-connection-string-url": "^2.6.0", + "socks": "^2.7.1" + }, + "engines": { + "node": ">=14.20.1" + }, + "optionalDependencies": { + "@mongodb-js/saslprep": "^1.1.0" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.0.0", + "kerberos": "^1.0.0 || ^2.0.0", + "mongodb-client-encryption": ">=2.3.0 <3", + "snappy": "^7.2.2" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + } + } + }, + "node_modules/mongodb-memory-server-core/node_modules/mongodb-connection-string-url": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", + "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", + "dev": true, + "dependencies": { + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^11.0.0" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/mongodb-memory-server-core/node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/mongodb-memory-server-core/node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/ms": { "version": "2.0.0", "license": "MIT" @@ -7760,6 +7958,12 @@ ], "license": "MIT" }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, "node_modules/quick-format-unescaped": { "version": "4.0.4", "dev": true, @@ -8315,6 +8519,19 @@ "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", "optional": true }, + "node_modules/streamx": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.1.tgz", + "integrity": "sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==", + "dev": true, + "dependencies": { + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "license": "MIT", @@ -8732,6 +8949,17 @@ "node": ">=10" } }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/tdigest": { "version": "0.1.1", "license": "MIT", @@ -9326,37 +9554,6 @@ "mongodb-memory-server": "^8.12.0" } }, - "node_modules/vitest-mongodb/node_modules/@types/whatwg-url": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-8.2.2.tgz", - "integrity": "sha512-FtQu10RWgn3D9U4aazdwIE2yzphmTJREDqNdODHrbrZmmMqI0vMheC/6NE/J1Yveaj8H+ela+YwWTjq5PGmuhA==", - "dev": true, - "dependencies": { - "@types/node": "*", - "@types/webidl-conversions": "*" - } - }, - "node_modules/vitest-mongodb/node_modules/async-mutex": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.2.tgz", - "integrity": "sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA==", - "dev": true, - "dependencies": { - "tslib": "^2.3.1" - } - }, - "node_modules/vitest-mongodb/node_modules/bson": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/bson/-/bson-4.7.2.tgz", - "integrity": "sha512-Ry9wCtIZ5kGqkJoi6aD8KjxFZEx78guTQDnpXWiNthsxzrxAK/i8E6pCHAIZTbaEFWcOCvbecMukfK7XUvyLpQ==", - "dev": true, - "dependencies": { - "buffer": "^5.6.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/vitest-mongodb/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -9374,130 +9571,12 @@ } } }, - "node_modules/vitest-mongodb/node_modules/mongodb": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.17.2.tgz", - "integrity": "sha512-mLV7SEiov2LHleRJPMPrK2PMyhXFZt2UQLC4VD4pnth3jMjYKHhtqfwwkkvS/NXuo/Fp3vbhaNcXrIDaLRb9Tg==", - "dev": true, - "dependencies": { - "bson": "^4.7.2", - "mongodb-connection-string-url": "^2.6.0", - "socks": "^2.7.1" - }, - "engines": { - "node": ">=12.9.0" - }, - "optionalDependencies": { - "@aws-sdk/credential-providers": "^3.186.0", - "@mongodb-js/saslprep": "^1.1.0" - } - }, - "node_modules/vitest-mongodb/node_modules/mongodb-connection-string-url": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.6.0.tgz", - "integrity": "sha512-WvTZlI9ab0QYtTYnuMLgobULWhokRjtC7db9LtcVfJ+Hsnyr5eo6ZtNAt3Ly24XZScGMelOcGtm7lSn0332tPQ==", - "dev": true, - "dependencies": { - "@types/whatwg-url": "^8.2.1", - "whatwg-url": "^11.0.0" - } - }, - "node_modules/vitest-mongodb/node_modules/mongodb-memory-server": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-8.16.0.tgz", - "integrity": "sha512-oaeu2GZWycIysTj18b1gZ6d+CqWeQQZe5f8ml8Z1buaGAn3GcrGdbG5+0fseEO5ANQzcjA92qHhbsImgXeEmIQ==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "mongodb-memory-server-core": "8.16.0", - "tslib": "^2.6.1" - }, - "engines": { - "node": ">=12.22.0" - } - }, - "node_modules/vitest-mongodb/node_modules/mongodb-memory-server-core": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-8.16.0.tgz", - "integrity": "sha512-wyNo8yj6se7KH49hQmRtiwide7DnGINUGa1m84RyX1NU9DkCrTwbOV2VbPgd3+55DZfRup/DebU1M1zEv+3Rng==", - "dev": true, - "dependencies": { - "async-mutex": "^0.3.2", - "camelcase": "^6.3.0", - "debug": "^4.3.4", - "find-cache-dir": "^3.3.2", - "follow-redirects": "^1.15.2", - "get-port": "^5.1.1", - "https-proxy-agent": "^5.0.1", - "md5-file": "^5.0.0", - "mongodb": "^4.16.0", - "new-find-package-json": "^2.0.0", - "semver": "^7.5.4", - "tar-stream": "^2.1.4", - "tslib": "^2.6.1", - "uuid": "^9.0.0", - "yauzl": "^2.10.0" - }, - "engines": { - "node": ">=12.22.0" - } - }, "node_modules/vitest-mongodb/node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/vitest-mongodb/node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/vitest-mongodb/node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "dev": true, - "dependencies": { - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest-mongodb/node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/vitest-mongodb/node_modules/whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", - "dev": true, - "dependencies": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/vitest/node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -9956,13 +10035,16 @@ } }, "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.1.3.tgz", + "integrity": "sha512-JCCdmlJJWv7L0q/KylOekyRaUrdEoUxWkWVcgorosTROCFWiS9p2NNPE9Yb91ak7b1N5SxAZEliWpspbZccivw==", "dev": true, "dependencies": { "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" } }, "node_modules/yn": { diff --git a/backend/package.json b/backend/package.json index 93785b499..4dba879b0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -81,5 +81,8 @@ "typescript": "5.3.3", "vitest": "1.6.0", "vitest-mongodb": "0.0.5" + }, + "overrides": { + "mongodb-memory-server": "9.2.0" } } diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts index 568d5186b..021848696 100644 --- a/backend/src/api/controllers/user.ts +++ b/backend/src/api/controllers/user.ts @@ -19,8 +19,6 @@ import { deleteConfig } from "../../dal/config"; import { verify } from "../../utils/captcha"; import * as LeaderboardsDAL from "../../dal/leaderboards"; import { purgeUserFromDailyLeaderboards } from "../../utils/daily-leaderboards"; -import { randomBytes } from "crypto"; -import * as RedisClient from "../../init/redis"; import { v4 as uuidv4 } from "uuid"; import { ObjectId } from "mongodb"; import * as ReportDAL from "../../dal/report"; @@ -32,6 +30,7 @@ import { } from "../../utils/auth"; import * as Dates from "date-fns"; import { UTCDateMini } from "@date-fns/utc"; +import * as BlocklistDal from "../../dal/blocklist"; async function verifyCaptcha(captcha: string): Promise { if (!(await verify(captcha))) { @@ -47,28 +46,30 @@ export async function createNewUser( try { await verifyCaptcha(captcha); - } catch (e) { - try { - await firebaseDeleteUser(uid); - } catch (e) { - // user might be deleted on the frontend + + if (email.endsWith("@tidal.lol") || email.endsWith("@selfbot.cc")) { + throw new MonkeyError(400, "Invalid domain"); } + + const available = await UserDAL.isNameAvailable(name, uid); + if (!available) { + throw new MonkeyError(409, "Username unavailable"); + } + + const blocklisted = await BlocklistDal.contains({ name, email }); + if (blocklisted) { + throw new MonkeyError(409, "Username or email blocked"); + } + + await UserDAL.addUser(name, email, uid); + void Logger.logToDb("user_created", `${name} ${email}`, uid); + + return new MonkeyResponse("User created"); + } catch (e) { + //user was created in firebase from the frontend, remove it + await firebaseDeleteUserIgnoreError(uid); throw e; } - - if (email.endsWith("@tidal.lol") || email.endsWith("@selfbot.cc")) { - throw new MonkeyError(400, "Invalid domain"); - } - - const available = await UserDAL.isNameAvailable(name, uid); - if (!available) { - throw new MonkeyError(409, "Username unavailable"); - } - - await UserDAL.addUser(name, email, uid); - void Logger.logToDb("user_created", `${name} ${email}`, uid); - - return new MonkeyResponse("User created"); } export async function sendVerificationEmail( @@ -182,10 +183,9 @@ export async function deleteUser( const userInfo = await UserDAL.getUser(uid, "delete user"); - // gdpr goes brr, find a different way - // if (userInfo.banned) { - // throw new MonkeyError(403, "Banned users cannot delete their account"); - // } + if (userInfo.banned === true) { + await BlocklistDal.add(userInfo); + } //cleanup database await Promise.all([ @@ -435,39 +435,24 @@ export async function getUser( export async function getOauthLink( req: MonkeyTypes.Request ): Promise { - const connection = RedisClient.getConnection(); - if (!connection) { - throw new MonkeyError(500, "Redis connection not found"); - } - const { uid } = req.ctx.decodedToken; - const token = randomBytes(10).toString("hex"); - - //add the token uid pair to reids - await connection.setex(`discordoauth:${uid}`, 60, token); //build the url - const url = DiscordUtils.getOauthLink(); + const url = await DiscordUtils.getOauthLink(uid); //return return new MonkeyResponse("Discord oauth link generated", { - url: `${url}&state=${token}`, + url: url, }); } export async function linkDiscord( req: MonkeyTypes.Request ): Promise { - const connection = RedisClient.getConnection(); - if (!connection) { - throw new MonkeyError(500, "Redis connection not found"); - } const { uid } = req.ctx.decodedToken; const { tokenType, accessToken, state } = req.body; - const redisToken = await connection.getdel(`discordoauth:${uid}`); - - if (!(redisToken ?? "") || redisToken !== state) { + if (!(await DiscordUtils.iStateValidForUser(state, uid))) { throw new MonkeyError(403, "Invalid user token"); } @@ -503,6 +488,10 @@ export async function linkDiscord( ); } + if (await BlocklistDal.contains({ discordId })) { + throw new MonkeyError(409, "The Discord account is blocked"); + } + await UserDAL.linkDiscord(uid, discordId, discordAvatar); await GeorgeQueue.linkDiscord(discordId, uid); @@ -1033,3 +1022,11 @@ export async function getTestActivity( return new MonkeyResponse("Test activity data retrieved", user.testActivity); } + +async function firebaseDeleteUserIgnoreError(uid: string): Promise { + try { + await firebaseDeleteUser(uid); + } catch (e) { + //ignore + } +} diff --git a/backend/src/api/routes/admin.ts b/backend/src/api/routes/admin.ts index 2abf72e77..058f2d420 100644 --- a/backend/src/api/routes/admin.ts +++ b/backend/src/api/routes/admin.ts @@ -22,7 +22,6 @@ router.use( invalidMessage: "Admin endpoints are currently disabled.", }) ); - router.get( "/", adminLimit, diff --git a/backend/src/dal/blocklist.ts b/backend/src/dal/blocklist.ts new file mode 100644 index 000000000..78e53daef --- /dev/null +++ b/backend/src/dal/blocklist.ts @@ -0,0 +1,116 @@ +import { Collection } from "mongodb"; +import * as db from "../init/db"; +import { createHash } from "crypto"; + +type BlocklistEntryProperties = Pick< + SharedTypes.User, + "name" | "email" | "discordId" +>; +// Export for use in tests +export const getCollection = (): Collection => + db.collection("blocklist"); + +export async function add(user: BlocklistEntryProperties): Promise { + const timestamp = Date.now(); + const inserts: Promise[] = []; + + const usernameHash = hash(user.name); + const emailHash = hash(user.email); + inserts.push( + getCollection().replaceOne( + { usernameHash }, + { + usernameHash, + timestamp, + }, + { upsert: true } + ), + getCollection().replaceOne( + { emailHash }, + { + emailHash, + timestamp, + }, + { upsert: true } + ) + ); + + if (user.discordId !== undefined && user.discordId !== "") { + const discordIdHash = hash(user.discordId); + inserts.push( + getCollection().replaceOne( + { discordIdHash }, + { + discordIdHash, + timestamp, + }, + { upsert: true } + ) + ); + } + await Promise.all(inserts); +} + +export async function remove( + user: Partial +): Promise { + const filter = getFilter(user); + if (filter.length === 0) return; + await getCollection().deleteMany({ $or: filter }); +} + +export async function contains( + user: Partial +): Promise { + const filter = getFilter(user); + if (filter.length === 0) return false; + + return ( + (await getCollection().countDocuments({ + $or: filter, + })) !== 0 + ); +} +export function hash(value: string): string { + return createHash("sha256").update(value.toLocaleLowerCase()).digest("hex"); +} + +function getFilter( + user: Partial +): Partial[] { + const filter: Partial[] = []; + if (user.email !== undefined) { + filter.push({ emailHash: hash(user.email) }); + } + if (user.name !== undefined) { + filter.push({ usernameHash: hash(user.name) }); + } + if (user.discordId !== undefined) { + filter.push({ discordIdHash: hash(user.discordId) }); + } + return filter; +} + +export async function createIndicies(): Promise { + await getCollection().createIndex( + { usernameHash: 1 }, + { + unique: true, + partialFilterExpression: { usernameHash: { $exists: true } }, + } + ); + await getCollection().createIndex( + { emailHash: 1 }, + { + unique: true, + partialFilterExpression: { emailHash: { $exists: true } }, + } + ); + await getCollection().createIndex( + { discordIdHash: 1 }, + { + unique: true, + partialFilterExpression: { discordIdHash: { $exists: true } }, + } + ); +} diff --git a/backend/src/server.ts b/backend/src/server.ts index 9f71f48de..0cbf9802a 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -14,6 +14,7 @@ import * as EmailClient from "./init/email-client"; import { init as initFirebaseAdmin } from "./init/firebase-admin"; import { createIndicies as leaderboardDbSetup } from "./dal/leaderboards"; +import { createIndicies as blocklistDbSetup } from "./dal/blocklist"; async function bootServer(port: number): Promise { try { @@ -68,6 +69,9 @@ async function bootServer(port: number): Promise { Logger.info("Setting up leaderboard indicies..."); await leaderboardDbSetup(); + Logger.info("Setting up blocklist indicies..."); + await blocklistDbSetup(); + recordServerVersion(version); } catch (error) { Logger.error("Failed to boot server"); diff --git a/backend/src/types/types.d.ts b/backend/src/types/types.d.ts index f767247bc..97a519669 100644 --- a/backend/src/types/types.d.ts +++ b/backend/src/types/types.d.ts @@ -110,4 +110,14 @@ declare namespace MonkeyTypes { type DBResult = MonkeyTypes.WithObjectId< SharedTypes.DBResult >; + + type BlocklistEntry = { + _id: string; + usernameHash?: string; + emailHash?: string; + discordIdHash?: string; + timestamp: number; + }; + + type DBBlocklistEntry = WithObjectId; } diff --git a/backend/src/utils/discord.ts b/backend/src/utils/discord.ts index 74971bbf0..b3b74381f 100644 --- a/backend/src/utils/discord.ts +++ b/backend/src/utils/discord.ts @@ -1,5 +1,8 @@ import fetch from "node-fetch"; import { isDevEnvironment } from "./misc"; +import * as RedisClient from "../init/redis"; +import { randomBytes } from "crypto"; +import MonkeyError from "./error"; const BASE_URL = "https://discord.com/api"; @@ -34,10 +37,32 @@ export async function getDiscordUser( return (await response.json()) as DiscordUser; } -export function getOauthLink(): string { +export async function getOauthLink(uid: string): Promise { + const connection = RedisClient.getConnection(); + if (!connection) { + throw new MonkeyError(500, "Redis connection not found"); + } + const token = randomBytes(10).toString("hex"); + + //add the token uid pair to reids + await connection.setex(`discordoauth:${uid}`, 60, token); + return `${BASE_URL}/oauth2/authorize?client_id=798272335035498557&redirect_uri=${ isDevEnvironment() ? `http%3A%2F%2Flocalhost%3A3000%2Fverify` : `https%3A%2F%2Fmonkeytype.com%2Fverify` - }&response_type=token&scope=identify`; + }&response_type=token&scope=identify&state=${token}`; +} + +export async function iStateValidForUser( + state: string, + uid: string +): Promise { + const connection = RedisClient.getConnection(); + if (!connection) { + throw new MonkeyError(500, "Redis connection not found"); + } + const redisToken = await connection.getdel(`discordoauth:${uid}`); + + return redisToken === state; } diff --git a/backend/vitest.config.js b/backend/vitest.config.js index 8a431a4b0..aacbb1a01 100644 --- a/backend/vitest.config.js +++ b/backend/vitest.config.js @@ -4,6 +4,7 @@ export default defineConfig({ test: { globals: true, environment: "node", + globalSetup: "__tests__/global-setup.ts", setupFiles: ["__tests__/setup-tests.ts"], pool: "forks", diff --git a/frontend/src/privacy-policy.html b/frontend/src/privacy-policy.html index 3860a1c7e..ec1f3c3db 100644 --- a/frontend/src/privacy-policy.html +++ b/frontend/src/privacy-policy.html @@ -112,7 +112,7 @@

Effective date: September 8, 2021

-

Last updated: Oct 24, 2023

+

Last updated: May 20, 2024

Thanks for trusting Monkeytype ('Monkeytype', 'we', 'us', 'our') with your personal information! We take our responsibility to you very @@ -177,6 +177,7 @@

  • Email
  • Username
  • +
  • Discord id and discord avatar id (if you provide it)
  • Information about each typing test
  • Your currently active settings
  • How many typing tests you've started and completed
  • @@ -221,6 +222,12 @@
  • Display leaderboards
+

+ If you are found to be cheating or exploiting the website, we may + store hashed versions of your username, email and/or discord id to + prevent you from creating new accounts. +

+

How do we store your data?

Monkeytype securely stores your data using MongoDB.

@@ -245,7 +252,10 @@
  • The right to erasure – You have the right to request that Monkeytype - erase your personal data, under certain conditions. + erase your personal data, under certain conditions. (Hashed data + mentioned in the "How will we use your data?" section will not be + deleted, as it is essential in preventing the exploitation of the + website)
  • The right to restrict processing – You have the right to request