From 5a6e122c258bd1502fe30325158799cb89ae0d06 Mon Sep 17 00:00:00 2001 From: Hadiqa Raza Date: Thu, 7 May 2026 17:20:50 +0300 Subject: [PATCH 1/9] feat(auth): implement forgot-password and reset-password endpoints --- package-lock.json | 47 +++++++- package.json | 2 + prisma/dev.db | Bin 0 -> 49152 bytes prisma/dev.db-journal | Bin 0 -> 8720 bytes .../20260507135532_init/migration.sql | 48 ++++++++ prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 6 +- src/controllers/auth.controller.ts | 22 +++- src/interfaces/auth.interface.ts | 2 +- src/routes/auth.routes.ts | 8 ++ src/schemas/auth.schema.ts | 8 ++ src/services/auth.service.ts | 114 +++++++++++++++--- src/test/health.test.ts | 15 +++ 13 files changed, 254 insertions(+), 21 deletions(-) create mode 100644 prisma/dev.db create mode 100644 prisma/dev.db-journal create mode 100644 prisma/migrations/20260507135532_init/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 src/test/health.test.ts diff --git a/package-lock.json b/package-lock.json index 6b43fac..2c5a6d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@fastify/rate-limit": "^10.3.0", "@fastify/sensible": "^6.0.0", "@prisma/client": "^5.19.1", + "bcrypt": "^6.0.0", "chalk": "^5.6.2", "dotenv": "^17.4.2", "fastify": "^5.0.0", @@ -24,6 +25,7 @@ "zod": "^4.3.6" }, "devDependencies": { + "@types/bcrypt": "^6.0.0", "@types/node": "^25.0.3", "c8": "^11.0.0", "concurrently": "^9.0.0", @@ -932,6 +934,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -1085,6 +1097,20 @@ "node": "18 || 20 || >=22" } }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/brace-expansion": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", @@ -1763,7 +1789,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -2218,6 +2243,26 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz", + "integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", diff --git a/package.json b/package.json index 6c21c53..6513816 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@fastify/rate-limit": "^10.3.0", "@fastify/sensible": "^6.0.0", "@prisma/client": "^5.19.1", + "bcrypt": "^6.0.0", "chalk": "^5.6.2", "dotenv": "^17.4.2", "fastify": "^5.0.0", @@ -32,6 +33,7 @@ "zod": "^4.3.6" }, "devDependencies": { + "@types/bcrypt": "^6.0.0", "@types/node": "^25.0.3", "c8": "^11.0.0", "concurrently": "^9.0.0", diff --git a/prisma/dev.db b/prisma/dev.db new file mode 100644 index 0000000000000000000000000000000000000000..20ce05b6bf9599e56c002b7aad283f6b84fbb839 GIT binary patch literal 49152 zcmeI&%WmsL7zc2>7pF}dm7SM$y`d45#z~r_3nWb9mWVX%aU9O+*;o^Ma%wdib@sk4!RMvx59}vDwtd&8z`gbI>(s_y+?t?ssb4mJT>n1x z?HafK>B?`(pM~#Meqhxo5P$##ATSpKr)z@vhRA>B^lW<3^r-8Zv`d_hc}_nLKdzT` zO*J&BS+726YErdc(Vj{2R7+VpuE&+~wj6dKr@!D=6XMnu|J9LCS{)i3V)V~x&%4=5 z#+@7LyEQH9AZj8@+ewKP*|BBG(4HAm{g}O*wVHIISC3TvRC-@KeKTq^WIb1>+r|4_ z>QVoMc-}{S*ADxR`nzqnb}?{Rds(Wm2^-ZTZPcjEs?Wxb9-U3xf>;*yyn7nh^rd6b zyH3(_De%6Kfdi(Mde^aA4ngaMYxHTyGeEKw`s)zMJRoa$g(X^y%2O1l-tTnE~%l5LEq=SH0w3@~;LuaR>SIb6F z)jSF8q_Wy5s}(Jo&Lsu$aEs@{^zuIJu)7zN4E=ui*}O>vQ@Pn*yD}09ad(%0^>mat zA5hn6I~MVsew05G6)Ur8G_n7|vdItfcPb&{Fx($u8l~2)5so`}Q0-6IH99f3lnqH#hk&%cIOthyGc3MF}^A*-QxQA1ng~@ulQF;+@@|2#*}c zaC(mtKSdBkZS%hPMduR6E!<38UMwa=QRJ_VNAU_{~4?~q$6|Fc*n)9EEad|SLv%Zo00-tyIRJ|QX!|H=%N%z^88U1D~f7jAUH zo2^-x^qXxkff=l%Qzcwe$ClF3dUJg>I-}OPH|)xj$4xB7Bk!K$ol)B)Z2h_^Q;X&@>*>?RPqCz!tI2$48wS(({SSmeuhVL8sTPMpGUwvDVOa zY0o!(r%Tzv-9bDY84NlO>&Wxzz%#A>uouphEvVX|rn6h423tC{%4~<3WV*Z{h)mI00Izz00bZa0SG_<0uX?}{0rdu|NO@oGlT#HAOHafKmY;|fB*y_009W#`#*93 Z0uX=z1Rwwb2tWV=5P$##ATa*|e*>kQ5qbas literal 0 HcmV?d00001 diff --git a/prisma/dev.db-journal b/prisma/dev.db-journal new file mode 100644 index 0000000000000000000000000000000000000000..00a94c1fbbbc408eef423d10cf74a5a2dd906619 GIT binary patch literal 8720 zcmeI$ISK+n6a>(o0TBh4i;VR0dpn4+^a_(0iJ5^H@d(D^6+DQ6p%-xQ3=;@e@#BJe z;b#o_bAfg-05q#38xDa01Rwwb2tWV=5P$##AOHafKwwmX^R$_7&kLp)o6Mx*R@Rid zB8=)NmeK_)ROczUQmXaN6rHNK#YkB*k0unyqsxJ;qh!2DK~T(&oT$NSo^4FQ_?U90 zB#4|0l1sxiA8NH$a@X(sLvv|w&)0i%?}usMKLA^gIud0NfB*y_009U<00Izz00bZa N0SNqncD literal 0 HcmV?d00001 diff --git a/prisma/migrations/20260507135532_init/migration.sql b/prisma/migrations/20260507135532_init/migration.sql new file mode 100644 index 0000000..0f31bdf --- /dev/null +++ b/prisma/migrations/20260507135532_init/migration.sql @@ -0,0 +1,48 @@ +-- CreateTable +CREATE TABLE "Users" ( + "id" TEXT NOT NULL PRIMARY KEY, + "email" TEXT NOT NULL, + "firstName" TEXT, + "lastName" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- CreateTable +CREATE TABLE "UserAuths" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL, + "passwordHash" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "UserAuths_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "UserVerifications" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL, + "token" TEXT NOT NULL, + "deviceId" TEXT NOT NULL, + "expiresAt" DATETIME NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "UserVerifications_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "UserTokens" ( + "id" TEXT NOT NULL PRIMARY KEY, + "userId" TEXT NOT NULL, + "resetPasswordToken" TEXT, + "resetPasswordExpires" DATETIME, + "refreshToken" TEXT NOT NULL, + "accessToken" TEXT NOT NULL, + "deviceId" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "UserTokens_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "Users_email_key" ON "Users"("email"); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..e5e5c47 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "sqlite" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 287b788..9c7fd4e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -9,8 +9,8 @@ generator client { } datasource db { - provider = "postgresql" - url = env("DATABASE_URL") + provider = "sqlite" + url = "file:./dev.db" } model Users { @@ -48,6 +48,8 @@ model UserVerifications { model UserTokens { id String @id @default(uuid()) userId String + resetPasswordToken String? + resetPasswordExpires DateTime? refreshToken String accessToken String deviceId String diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index 8275fc9..c7615fc 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -1,8 +1,9 @@ import authService from "../services/auth.service"; import {FastifyReply, FastifyRequest} from "fastify"; import {sendResponse} from "../helpers"; -import {LoginRequest} from "../schemas"; +//import {LoginRequest} from "../schemas"; import AuthService from "../services/auth.service"; +import {LoginRequest, ForgotPasswordRequest, ResetPasswordRequest} from "../schemas"; class AuthController { constructor() { @@ -22,6 +23,25 @@ class AuthController { }) return sendResponse(reply, result) } + public static async forgotPassword(request: FastifyRequest, reply: FastifyReply) { + const { email } = ForgotPasswordRequest.parse(request.body ?? {}); + const result = await AuthService.forgotPassword({ + deviceId: request.headers['x-device-id'], + email, + }); + return sendResponse(reply, result); + } + + public static async resetPassword(request: FastifyRequest, reply: FastifyReply) { + const { token, newPassword } = ResetPasswordRequest.parse(request.body ?? {}); + const result = await AuthService.resetPassword({ + deviceId: request.headers['x-device-id'], + token, + newPassword, + }); + return sendResponse(reply, result); + } + } diff --git a/src/interfaces/auth.interface.ts b/src/interfaces/auth.interface.ts index dce1073..2dee147 100644 --- a/src/interfaces/auth.interface.ts +++ b/src/interfaces/auth.interface.ts @@ -23,7 +23,7 @@ export interface RefreshTokenDTO extends DeviceId { } export interface ResetPasswordDTO extends DeviceId{ - authToken: string; + token: string; newPassword: string; } diff --git a/src/routes/auth.routes.ts b/src/routes/auth.routes.ts index ac48cb9..c0fb7c9 100644 --- a/src/routes/auth.routes.ts +++ b/src/routes/auth.routes.ts @@ -5,4 +5,12 @@ AuthenticationController.initialize(); export async function AuthRouter(app: FastifyInstance) { app.post("/v1/auth/login", async (request: FastifyRequest, reply: FastifyReply) => AuthenticationController.login(request, reply)); + + app.post("/v1/auth/forgot-password", async (request: FastifyRequest, reply: FastifyReply) => + AuthenticationController.forgotPassword(request, reply) + ); + + app.post("/v1/auth/reset-password", async (request: FastifyRequest, reply: FastifyReply) => + AuthenticationController.resetPassword(request, reply) + ); } \ No newline at end of file diff --git a/src/schemas/auth.schema.ts b/src/schemas/auth.schema.ts index fc37655..4bf5e5f 100644 --- a/src/schemas/auth.schema.ts +++ b/src/schemas/auth.schema.ts @@ -5,3 +5,11 @@ export const LoginRequest = z.object({ password: z.string().min(8).max(128), }); +export const ForgotPasswordRequest = z.object({ + email: z.string().email().min(4).max(255), +}); + +export const ResetPasswordRequest = z.object({ + token: z.string().min(32), + newPassword: z.string().min(8).max(128), +}); diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 7463cd9..39cda82 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -1,15 +1,17 @@ +import crypto from "crypto"; +import bcrypt from "bcrypt"; import { ForgotPasswordDTO, IService, LoginDTO, RefreshTokenDTO, + ResetPasswordDTO, SignupDTO, VerifyDeviceChangeOTPDTO } from "../interfaces"; import {prisma} from "../lib/db"; -import {BadRequestError, CustomErrorCode} from "../exceptions"; - +import {BadRequestError, CustomErrorCode, NotFoundError} from "../exceptions"; // Souce of Truth -> Database class AuthService { @@ -78,6 +80,99 @@ class AuthService { } } } + public static async forgotPassword(input: ForgotPasswordDTO): Promise { + const { email, deviceId } = input; + + // Check if user exists + const user = await prisma.users.findUnique({ + where: { email } + }); + + // For security, don't reveal if email exists or not + if (!user) { + return { + success: true, + message: "If an account exists, a password reset link has been sent to your email", + } + } + + // Generate a secure random token + const resetToken = crypto.randomBytes(32).toString('hex'); + + // Token expires in 1 hour + const expiresAt = new Date(Date.now() + 60 * 60 * 1000); + + // Store token in database + await prisma.userToken.upsert({ + where: { userId: user.id }, + update: { + resetPasswordToken: resetToken, + resetPasswordExpires: expiresAt, + }, + create: { + userId: user.id, + resetPasswordToken: resetToken, + resetPasswordExpires: expiresAt, + }, + }); + + // Log token for testing (remove in production) + console.log(`\n 🔐 RESET TOKEN FOR ${email}: ${resetToken}\n`); + + return { + success: true, + message: "If an account exists, a password reset link has been sent to your email", + } + } + public static async resetPassword(input: ResetPasswordDTO): Promise { + const { token, newPassword, deviceId } = input; + + // Find the token in database + const tokenRecord = await prisma.userToken.findFirst({ + where: { + resetPasswordToken: token, + resetPasswordExpires: { + gt: new Date(), + }, + }, + include: { + user: true, + }, + }); + + // Check if token exists and is valid + if (!tokenRecord) { + throw new BadRequestError({ + msg: "Invalid or expired reset token", + errorCode: CustomErrorCode.AUTH_INVALID, + }); + } + + // Hash the new password + const saltRounds = 10; + const hashedPassword = await bcrypt.hash(newPassword, saltRounds); + + // Update user's password + await prisma.users.update({ + where: { id: tokenRecord.userId }, + data: { password: hashedPassword }, + }); + + // Delete the used token + await prisma.userToken.update({ + where: { userId: tokenRecord.userId }, + data: { + resetPasswordToken: null, + resetPasswordExpires: null, + }, + }); + + return { + success: true, + message: "Password has been reset successfully", + } + } + // Refresh Tokens -> These are tokens use in the background to keep the user logged in without them having to re-enter their credentials. @@ -94,20 +189,7 @@ class AuthService { } } - public static async forgotPassword(input: ForgotPasswordDTO): Promise { - return { - success: true, - message: "Password reset link sent to your email", - } - } - - public static async resetPassword(input: ForgotPasswordDTO): Promise { - return { - success: true, - message: "Password reset successful", - } - } - + } diff --git a/src/test/health.test.ts b/src/test/health.test.ts new file mode 100644 index 0000000..a037c7e --- /dev/null +++ b/src/test/health.test.ts @@ -0,0 +1,15 @@ +import { buildApp } from "../app"; // adjust path if needed + +test("GET /health should return ok", async () => { + const app = await buildApp(); + + const response = await app.inject({ + method: "GET", + url: "/health" + }); + + expect(response.statusCode).toBe(200); + expect(JSON.parse(response.body)).toEqual({ + status: "ok" + }); +}); \ No newline at end of file From b1f1fb50a66dc7b4de298ab37997941c96246d20 Mon Sep 17 00:00:00 2001 From: Hadiqa Raza Date: Wed, 13 May 2026 12:46:51 +0300 Subject: [PATCH 2/9] chore: update jwt helper, add app.ts, update tests --- prisma/dev.db | Bin 49152 -> 57344 bytes prisma/dev.db-journal | Bin 8720 -> 0 bytes src/app.ts | 7 +++++++ src/helpers/jwt.ts | 31 +++++++++++++++++++------------ src/test/health.test.ts | 30 ++++++++++++++++-------------- 5 files changed, 42 insertions(+), 26 deletions(-) delete mode 100644 prisma/dev.db-journal create mode 100644 src/app.ts diff --git a/prisma/dev.db b/prisma/dev.db index 20ce05b6bf9599e56c002b7aad283f6b84fbb839..cda1318cfa0a2f1cf64e13e399fe8f5cfca0890a 100644 GIT binary patch literal 57344 zcmeI5OKjW78Gt1_wi73gx-D8A7C{s@faC7cCWoX*9tx}?(T-%1vLwoSTm(Lb7G=F6 zQL!YT-KNR9yJ)XPZ+lr3D9{!~Q54vsD9}?Ed+4!6fxWi{3iRA#hm`z~AAxtfyG@LL zU`yo8a30_P&(G0tW|+^W9T!LJcDJd!5jOCZKrk41D-sC=0(Z#&ZSsHULge7u;R*Q; zo;bef@J?Xu$-7H81A(P$9|V^E8v4!LHrI_3#dC5@lOtN}VOowXPlYW<9xO(PTXwAF zh$fe`h?*h)g>*Wyol7dQTsg8Om){)6>67G>r{hh+I(o0S-|kwE^j_^$N|P8}Y_@k> zP7hlN{KPSF?^Lu%Lf(iK(pqF?Wpaht#kz~Fn7bTFkP2u?MLsUxVeEJzmy=bk>V@XD zn6iCX*p7AK#p5XPjGEVSvB#l>uy8N?&0CF=fZ?#K}}{0 zawMrH2&bz$le_WdumGAedM1-5V z(>Mio0>pN@Jy+G6cpN<$gb$!|wK^HTh{z$!jWD9&{Yyaojv6LhWLkc-Kx` zebjCyF?5qOz}^cYSbiJ5sI`A#3NkLm?PLQwY+E*Ik;{>dOioU2s-Ag@9Jj1U zPTr7-J&4Qs@udXL1JAgvdaW@br)6RYbHq^Ol5x%RDr|eEB@&P2+|8l`ipd7x=g-GOnKc{ zss+N~yLW@{#E&$?yFoW^FNFEK!KYt4vSR*i`>tN}8dDux`|1~JwnG02 z#FdqcBs|sSt1?rkx5=f~y}t44wfXS3!xtst$+tH)dcC=4mEb~{VS)!$UpcCsuG4Gk z)uyxC9UHu}J~Pu6?`(uAljn6wPdUGHdZJHy+MJm+Z0_~?&E@IqruwuK!(}_98(PCw zRVO{mV|UocoWwe+O||8BnkEs?OR&-2CHI*#&Wk!ptlMrhNQQ=P?w!o_RF*wg?~)Gh z$rxUuhp~>TeAHDo``s>XxmDL`Vsde_Gs#ZxbQ%sxsORENuWGjYEx)EjG38A;7b&QD za_f{6XA`6sSK~AD;UaU99-I|il{xD-OZ3TCswHv|40QtJ4;&Bx0zd!=00AHX1b_e# z00KY&2mk>f@Hr89x)fY2JYAY&1A)04h@}N&nm3qM!vuS0U!VbfB+Bx0zd!=00AHX1b_e#c!>$vvo{MX z;jhol1@3(_fQ2YxS)@~@#M=~ykwl3Wj#2_^NS4lum|=Lb!s0EL%*bcf$O^}tx1vuY zXTkR)AAEFc{Hb;IC%9qgJjk`ehlKcnD@jF6yPjG@yM9VK<)Y{v3t2p~!nL=zDw z(yZ<;J3Kd|ci(eJM&$F{jJ)Unv!Mg>{6BOM`qfJ;VkjCA00KY&2mk>f00e*l5C8%| z00;nqSDwIw*+TF#!xpn%#LJ93%=pj$FEMTazyDu(<$($S0U!VbfB+Bx0zd!=00AHX z1c1ORLjZpNzcQkNf&u{`00e*l5C8%|00;m9AOHk_01)`h1pa@&|GoA9K^CfXV1i&-yXLmice%19ZO^GrRX?KIO`uA$WT+{|XK zp*l3Hpd??}lt-D(!a&gqPCA}y7{!f_;U(JS8y5YLu9Q?-$@6ss(O&porBvyZ(N3!3 z#cAalCYyCDn>$D^s*SWJ50xP)ShG>HNVuYzY$j1gYC>Vv_`Xvq)%KJ1_COt(yizao zB%HRhYa6-JV^xSf9`>b;qSh@Hikxeu8ARLBhq@_0srHAXO%CPLM(?U&;M97VQR+U1 zWFHp>ZK||2Fbc_xnac}%rLAHsXKrNJ_~VT5u+uV|EZ1nFthYTkW#K1|i7B!L_tDz9$_CLN+f6J49=xUZk-VyFiOQ%yVkv345S#`9F)OwOpMtejrcQ~)X z_E~#8=Bbj2^_7nLEaM+#Z zK-#A=%8oLR=(V^rY7ez&n$;?a&30Lh>UgRy7M$%=rapx_B4I4?kE^v~K zELxN}WEzO^th6W!QPCE4)@Cq6M_HZY1&QW#&J;w!#5Bhmq?P>f_n&+Q0Vank+SpTy zshSd}_lYv`#E>?XV%E)Qxtik8oI0x2m3nfh)OYt4B%e~`RD&30`g2m`p?a<&$5>C1 zQCdWKCO?Wdb&>9m)NDPL&v3Qa&aUou(T-B|93#%008^{U%4J1 zR0s$F0U!VbfB+Bx0zd!=00AHX1YRZr-unN`jK%x=|6gXfbG-lmB?hI&zyCiDggy@a zJM_uR#01I$1b_e#00KY&2mk>f00e*l5C8%|00^8R@Y?K3u!Z;E@}BiGYi7GS`_f00e*l5C8%|00;nq zD@)+%%ItNrCTxx@q!A=!vOL33JR3DBj*jw_L7O7QNsL5Gm=h3fowFS5%tEgF$hgnY z_seFcw4Jb7r#wihX~mTv?JMj3?R8}^TCchFEn!D5<@CMuo@>Msds`_ZV-)qXbJaii z=oVSl@Y5gu<>|`IVM%$GW@yYJ%8u$B#o0EZ$jS(oqFKX^Vq}>*i!Za}XHwFHW?5{b zWC>cU->Ic~3S-oSHQJSjR&#xw+suwqT5H!>+kPBtx=|dj3CLMHThhbjA>pSh!NZaw zQ!pe`&?zL^I>m8OgEBapro^a??Wl>cfYD`EhWI5#_vwPYU*FJ%&0#y1V#=L(l3y?4 z6lcWa_FyYLD7(DFSv4KEa7;ysSaWxjKc}SQ`TykeX#PK${U18GvPupW0|Gz*2mk>f o00e*l5C8%|00;m9An+0p@aF$tX7I(I|9_dWkMaEfOAI~y7jIlSO#lD@ delta 180 zcmZoTz}(QlJV9EJje&uI2Z&*SZK95`G8=hBMeE-{x+b ze1|iC@;{y|HbXWxaaZ}t8+m0WuizHoW^b%uW)~M1XKePLtibw#QFHTZ-bkgzECmWM NyqTrokNlzr0RUVTGid+- diff --git a/prisma/dev.db-journal b/prisma/dev.db-journal deleted file mode 100644 index 00a94c1fbbbc408eef423d10cf74a5a2dd906619..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8720 zcmeI$ISK+n6a>(o0TBh4i;VR0dpn4+^a_(0iJ5^H@d(D^6+DQ6p%-xQ3=;@e@#BJe z;b#o_bAfg-05q#38xDa01Rwwb2tWV=5P$##AOHafKwwmX^R$_7&kLp)o6Mx*R@Rid zB8=)NmeK_)ROczUQmXaN6rHNK#YkB*k0unyqsxJ;qh!2DK~T(&oT$NSo^4FQ_?U90 zB#4|0l1sxiA8NH$a@X(sLvv|w&)0i%?}usMKLA^gIud0NfB*y_009U<00Izz00bZa N0SNqncD diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..3439c90 --- /dev/null +++ b/src/app.ts @@ -0,0 +1,7 @@ +import { buildServer } from "./server"; + +export async function buildApp() { + const app = buildServer(); + await app.ready(); + return app; +} \ No newline at end of file diff --git a/src/helpers/jwt.ts b/src/helpers/jwt.ts index fd66ea1..13767d8 100644 --- a/src/helpers/jwt.ts +++ b/src/helpers/jwt.ts @@ -1,5 +1,5 @@ import jwt from 'jsonwebtoken'; -import {config} from '../config'; +import { config } from '../config'; export enum TOKEN_TYPE { AUTH_TOKEN = 'AUTH_TOKEN', @@ -14,23 +14,30 @@ export interface TokenPayload { tokenType: TOKEN_TYPE; } +const JWT_SECRET = config.jwt.secret; +const ACCESS_EXPIRES = config.jwt.accessExpiresIn; +const REFRESH_EXPIRES = config.jwt.refreshExpiresIn; + export function generateJwtToken(payload: TokenPayload): string { + if (!JWT_SECRET) { + throw new Error('JWT_SECRET is not defined in config'); + } + + const { tokenType, ...cleanPayload } = payload; + if (payload.tokenType === TOKEN_TYPE.AUTH_TOKEN) { - return jwt.sign(payload, config.jwt.secret, { - expiresIn: config.jwt.accessExpiresIn as jwt.SignOptions['expiresIn'], - }); + return jwt.sign(cleanPayload, JWT_SECRET, { expiresIn: ACCESS_EXPIRES as any }); } else if (payload.tokenType === TOKEN_TYPE.REFRESH_TOKEN) { - return jwt.sign(payload, config.jwt.secret, { - expiresIn: config.jwt.refreshExpiresIn as jwt.SignOptions['expiresIn'], - }); + return jwt.sign(cleanPayload, JWT_SECRET, { expiresIn: REFRESH_EXPIRES as any }); } else if (payload.tokenType === TOKEN_TYPE.RESET_TOKEN) { - return jwt.sign(payload, config.jwt.secret, { - expiresIn: '1h', - }); + return jwt.sign(cleanPayload, JWT_SECRET, { expiresIn: '1h' as any }); } throw new Error('Invalid token type'); } export function verifyToken(token: string): TokenPayload { - return jwt.verify(token, config.jwt.secret) as TokenPayload; -} + if (!JWT_SECRET) { + throw new Error('JWT_SECRET is not defined in config'); + } + return jwt.verify(token, JWT_SECRET) as TokenPayload; +} \ No newline at end of file diff --git a/src/test/health.test.ts b/src/test/health.test.ts index a037c7e..54f7ad3 100644 --- a/src/test/health.test.ts +++ b/src/test/health.test.ts @@ -1,15 +1,17 @@ -import { buildApp } from "../app"; // adjust path if needed +import { describe, it, expect } from "@jest/globals"; +import { buildServer } from "../server"; -test("GET /health should return ok", async () => { - const app = await buildApp(); - - const response = await app.inject({ - method: "GET", - url: "/health" - }); - - expect(response.statusCode).toBe(200); - expect(JSON.parse(response.body)).toEqual({ - status: "ok" - }); -}); \ No newline at end of file +describe("Health Check", () => { + it("GET /health should return 200", async () => { + const app = buildServer(); + + // Wait for server to be ready + await app.ready(); + + const response = await app.inject({ + method: "GET", + url: "/health" + }); + + expect(response.statusCode).toBe(200); +})}); \ No newline at end of file From 45c52d8834e32e2cbce5314bb87c292e88945061 Mon Sep 17 00:00:00 2001 From: Hadiqa Raza Date: Wed, 13 May 2026 13:05:47 +0300 Subject: [PATCH 3/9] feat: implement forgot and reset password conflicts --- prisma/dev.db | Bin 57344 -> 57344 bytes src/controllers/auth.controller.ts | 46 ++++--- src/routes/auth.routes.ts | 25 ++-- src/schemas/auth.schema.ts | 8 ++ src/services/auth.service.ts | 211 +++++++++++++++++++---------- 5 files changed, 190 insertions(+), 100 deletions(-) diff --git a/prisma/dev.db b/prisma/dev.db index cda1318cfa0a2f1cf64e13e399fe8f5cfca0890a..29a9b36b28e9dd17e17841122bece9ef78f0a12d 100644 GIT binary patch delta 1345 zcmd6n&1(}u7{-%k7sV!7dk~chRvM@vlkUgtM-bGwZMG@9O*Y+4lR&}U*;$)3X=0M5 zwjiw#^{V8cCl%^ZS|xhcliEv4;}=-59t1CQcp2V#=6#>vv-Ui&_B?QN z&_8_ZnCu^ZeBr@pkO=2|)NLvnei6=f2M$hzSIb1d$samIlD-qezL?|Mjw?vOksKaC zi-jNtB?n-~MlOafjj+|kjpkwl*U#b0R;5BIsElCEDCJ*%^Bbi64b1ZdP~hx*QrfUmPUb-0n@)1HN?)v4znHU zY^1XPQ=jfeH9Hg}$aP;xJn(csPp*3(Q_0n9St4YbS3_hQw81Ho6d{Lc7jb}RT?s(M z3c!L$0vOT^&p;ZA7T#qL*C--rnm*s(b~*ls?RMVZg$7?x3dMqwXHzmWpHg&2H8OKr zNpGeM9TsG!Tu>%Ug^EcRO6N<3nen13PnPYOM9r3yLUJKfMqGwLY<5{!vM@ixEZK_L znZ0*5nP;?;)<{;e5P3H>192%M$!UhRtZ{TS*3xtH+VoSL)qvTJg6{GZBP-9_eDo}F6JzsOv1SwmfnPw8$#sfk?4s5UZoVktf6plVi- zE6qwCnrSFBQF4bB;NVsfVc=2l9gi*bb^A)7x4^;npl1Wt7Hxv{RQ zN5v*J`-6$S*rmtBr`sMAn>$QA@=;sVN9qyvt~)SvpE`OZ!25TjnCNUhI~^oKUPNTW z55s+-FA))1y*fhd_bf|#GGl_ovApO2F9}31AOr*?o5h}LiGczg6M4(t#jU@DNQAW` z3>_I~qhdVD@ukctGqG%{qBNSj7@c?M=}R_B$t9dFD*01UNtKf>_i!7M%j%COZ2&z`HnI0Pv_U;JGNO+Uamf*C2LvlAS$ldpY0LDd24*&oF diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index c7615fc..dfdeaa2 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -1,32 +1,45 @@ -import authService from "../services/auth.service"; -import {FastifyReply, FastifyRequest} from "fastify"; -import {sendResponse} from "../helpers"; -//import {LoginRequest} from "../schemas"; import AuthService from "../services/auth.service"; -import {LoginRequest, ForgotPasswordRequest, ResetPasswordRequest} from "../schemas"; +import { FastifyReply, FastifyRequest } from "fastify"; +import { sendResponse } from "../helpers"; +import { LoginRequest, SignupRequest, ForgotPasswordRequest, ResetPasswordRequest } from "../schemas"; class AuthController { + static initialize() { + new AuthController(); + } + constructor() { - authService.initialize(); + AuthService.initialize(); } - static initialize() { - new AuthController(); + public static async signup(request: FastifyRequest, reply: FastifyReply) { + const { email, password, firstName, lastName, phone, company } = SignupRequest.parse(request.body ?? {}); + const result = await AuthService.signup({ + email, + password, + firstName, + lastName, + phone, + company: company || '', + deviceId: request.headers['x-device-id'] as string, + }); + return sendResponse(reply, result, 201); } public static async login(request: FastifyRequest, reply: FastifyReply) { - const {email, password} = LoginRequest.parse(request.body ?? {}); + const { email, password } = LoginRequest.parse(request.body ?? {}); const result = await AuthService.login({ - deviceId: request.headers['x-device-id'], + deviceId: request.headers['x-device-id'] as string, email, password, - }) - return sendResponse(reply, result) + }); + return sendResponse(reply, result); } + public static async forgotPassword(request: FastifyRequest, reply: FastifyReply) { const { email } = ForgotPasswordRequest.parse(request.body ?? {}); const result = await AuthService.forgotPassword({ - deviceId: request.headers['x-device-id'], + deviceId: request.headers['x-device-id'] as string, email, }); return sendResponse(reply, result); @@ -35,15 +48,12 @@ class AuthController { public static async resetPassword(request: FastifyRequest, reply: FastifyReply) { const { token, newPassword } = ResetPasswordRequest.parse(request.body ?? {}); const result = await AuthService.resetPassword({ - deviceId: request.headers['x-device-id'], + deviceId: request.headers['x-device-id'] as string, token, newPassword, }); return sendResponse(reply, result); } - - - } -export const AuthenticationController = AuthController \ No newline at end of file +export const AuthenticationController = AuthController; \ No newline at end of file diff --git a/src/routes/auth.routes.ts b/src/routes/auth.routes.ts index 51f5f37..d721676 100644 --- a/src/routes/auth.routes.ts +++ b/src/routes/auth.routes.ts @@ -1,17 +1,22 @@ -import {FastifyInstance, FastifyReply, FastifyRequest,} from "fastify"; -import {AuthenticationController} from "../controllers"; +import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify"; +import { AuthenticationController } from "../controllers"; AuthenticationController.initialize(); export async function AuthRouter(app: FastifyInstance) { - app.post("/v1/auth/signup", async (request: FastifyRequest, reply: FastifyReply) => AuthenticationController.signup(request, reply)); - app.post("/v1/auth/login", async (request: FastifyRequest, reply: FastifyReply) => AuthenticationController.login(request, reply)); + app.post("/v1/auth/signup", async (request: FastifyRequest, reply: FastifyReply) => { + return AuthenticationController.signup(request, reply); + }); + + app.post("/v1/auth/login", async (request: FastifyRequest, reply: FastifyReply) => { + return AuthenticationController.login(request, reply); + }); - app.post("/v1/auth/forgot-password", async (request: FastifyRequest, reply: FastifyReply) => - AuthenticationController.forgotPassword(request, reply) - ); + app.post("/v1/auth/forgot-password", async (request: FastifyRequest, reply: FastifyReply) => { + return AuthenticationController.forgotPassword(request, reply); + }); - app.post("/v1/auth/reset-password", async (request: FastifyRequest, reply: FastifyReply) => - AuthenticationController.resetPassword(request, reply) - ); + app.post("/v1/auth/reset-password", async (request: FastifyRequest, reply: FastifyReply) => { + return AuthenticationController.resetPassword(request, reply); + }); } \ No newline at end of file diff --git a/src/schemas/auth.schema.ts b/src/schemas/auth.schema.ts index 4bf5e5f..0456f23 100644 --- a/src/schemas/auth.schema.ts +++ b/src/schemas/auth.schema.ts @@ -1,4 +1,12 @@ import {z} from "zod"; +export const SignupRequest = z.object({ + email: z.string().email().min(4).max(255), + password: z.string().min(8).max(128), + firstName: z.string().min(2).max(100), + lastName: z.string().min(2).max(100), + phone: z.string().min(10).max(15), + company: z.string().optional(), +}); export const LoginRequest = z.object({ email: z.email().min(4).max(255), diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 39cda82..3f4649a 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -10,9 +10,9 @@ import { VerifyDeviceChangeOTPDTO } from "../interfaces"; -import {prisma} from "../lib/db"; -import {BadRequestError, CustomErrorCode, NotFoundError} from "../exceptions"; -// Souce of Truth -> Database +import { prisma } from "../lib/db"; +import { BadRequestError, CustomErrorCode, UnAuthorizedError } from "../exceptions"; +import { generateJwtToken, TOKEN_TYPE, hashPassword, verifyPassword } from "../helpers"; class AuthService { @@ -21,52 +21,129 @@ class AuthService { } public static async signup(input: SignupDTO): Promise { + const { email, password, firstName, lastName, phone, company, deviceId } = input; - const {email} = input; - const existingUser = await prisma.users.findUnique({ - where: { - email - } - }); - + const existingUser = await prisma.users.findUnique({ where: { email } }); if (existingUser) { throw new BadRequestError({ msg: "Account with the email already exists", errorCode: CustomErrorCode.DUPLICATE_RESOURCE }); } - // internals of this is that, const allocate a fixed memory space in the heap for the object we aare assigning it to. - // let and var are different from const in that, they can be re-assigned to a different value or object, while const cannot be re-assigned. However, the properties of an object assigned to a const variable can still be modified. + const passwordHash = await hashPassword(password); + + const { user, accessToken, refreshToken } = await prisma.$transaction(async (tx) => { + const user = await tx.users.create({ + data: { email, firstName, lastName, phone, company }, + }); + + await tx.userAuths.create({ + data: { userId: user.id, passwordHash, recognisedDevices: deviceId }, + }); + + const accessToken = generateJwtToken({ + userId: user.id, + email: user.email, + deviceId, + tokenType: TOKEN_TYPE.AUTH_TOKEN + }); + const refreshToken = generateJwtToken({ + userId: user.id, + email: user.email, + deviceId, + tokenType: TOKEN_TYPE.REFRESH_TOKEN + }); + + await tx.userTokens.create({ + data: { userId: user.id, deviceId, accessToken, refreshToken }, + }); + + return { user, accessToken, refreshToken }; + }); return { success: true, message: "Signup successful", - data: {} - } + data: { + accessToken, + refreshToken, + user, + }, + }; } public static async login(input: LoginDTO): Promise { + const { email, password, deviceId } = input; - //check if the user email is valid and verify the user exist in the database - // verify they are using the right password... - // verify the deviceId is a recognised device, if not, - // we send an email OTP to the user email to let them know that an - // unrecognised device is trying to access their account from a location - // that is not recognised. - // if the device is not recognised, we block the user from logging in and sent them email OTP to verify their identity. + const user = await prisma.users.findUnique({ where: { email } }); + if (!user) { + throw new UnAuthorizedError({ + msg: "Invalid email or password", + errorCode: CustomErrorCode.AUTH_INVALID, + }); + } + + const userAuth = await prisma.userAuths.findFirst({ + where: { userId: user.id }, + }); + if (!userAuth) { + throw new UnAuthorizedError({ + msg: "Invalid email or password", + errorCode: CustomErrorCode.AUTH_INVALID, + }); + } + + const passwordMatch = await verifyPassword(password, userAuth.passwordHash); + if (!passwordMatch) { + throw new UnAuthorizedError({ + msg: "Invalid email or password", + errorCode: CustomErrorCode.AUTH_INVALID, + }); + } - // if all are true, we generate a JWT tokens and return it to the client. + const isRecognisedDevice = userAuth.recognisedDevices === deviceId; + if (!isRecognisedDevice) { + const otp = Math.floor(100000 + Math.random() * 900000).toString(); + const expiresAt = new Date(Date.now() + 10 * 60 * 1000); + + await prisma.userVerifications.create({ + data: { userId: user.id, token: otp, deviceId, expiresAt }, + }); + + throw new UnAuthorizedError({ + msg: "Unrecognised device. A verification code has been sent to your email.", + errorCode: CustomErrorCode.AUTH_BLOCKED, + }); + } + + const accessToken = generateJwtToken({ + userId: user.id, + email: user.email, + deviceId, + tokenType: TOKEN_TYPE.AUTH_TOKEN + }); + const refreshToken = generateJwtToken({ + userId: user.id, + email: user.email, + deviceId, + tokenType: TOKEN_TYPE.REFRESH_TOKEN + }); + + await prisma.userTokens.updateMany({ + where: { userId: user.id, deviceId }, + data: { accessToken, refreshToken }, + }); return { success: true, message: "Login successful", data: { - accessToken: "", - refreshToken: "", - user: {} - } - } + accessToken, + refreshToken, + user, + }, + }; } public static async verifyDeviceChange(input: VerifyDeviceChangeOTPDTO): Promise { @@ -80,15 +157,12 @@ class AuthService { } } } + public static async forgotPassword(input: ForgotPasswordDTO): Promise { const { email, deviceId } = input; - // Check if user exists - const user = await prisma.users.findUnique({ - where: { email } - }); + const user = await prisma.users.findUnique({ where: { email } }); - // For security, don't reveal if email exists or not if (!user) { return { success: true, @@ -96,27 +170,35 @@ class AuthService { } } - // Generate a secure random token const resetToken = crypto.randomBytes(32).toString('hex'); - - // Token expires in 1 hour const expiresAt = new Date(Date.now() + 60 * 60 * 1000); - // Store token in database - await prisma.userToken.upsert({ - where: { userId: user.id }, - update: { - resetPasswordToken: resetToken, - resetPasswordExpires: expiresAt, - }, - create: { - userId: user.id, - resetPasswordToken: resetToken, - resetPasswordExpires: expiresAt, - }, + // Use userTokens table (plural) + const existingToken = await prisma.userTokens.findFirst({ + where: { userId: user.id, deviceId } }); + + if (existingToken) { + await prisma.userTokens.update({ + where: { id: existingToken.id }, + data: { + resetPasswordToken: resetToken, + resetPasswordExpires: expiresAt, + } + }); + } else { + await prisma.userTokens.create({ + data: { + userId: user.id, + deviceId, + resetPasswordToken: resetToken, + resetPasswordExpires: expiresAt, + refreshToken: crypto.randomBytes(40).toString('hex'), + accessToken: crypto.randomBytes(32).toString('hex') + } + }); + } - // Log token for testing (remove in production) console.log(`\n 🔐 RESET TOKEN FOR ${email}: ${resetToken}\n`); return { @@ -124,23 +206,18 @@ class AuthService { message: "If an account exists, a password reset link has been sent to your email", } } + public static async resetPassword(input: ResetPasswordDTO): Promise { const { token, newPassword, deviceId } = input; - // Find the token in database - const tokenRecord = await prisma.userToken.findFirst({ + const tokenRecord = await prisma.userTokens.findFirst({ where: { resetPasswordToken: token, - resetPasswordExpires: { - gt: new Date(), - }, - }, - include: { - user: true, + resetPasswordExpires: { gt: new Date() }, }, + include: { user: true }, }); - // Check if token exists and is valid if (!tokenRecord) { throw new BadRequestError({ msg: "Invalid or expired reset token", @@ -148,19 +225,16 @@ class AuthService { }); } - // Hash the new password const saltRounds = 10; const hashedPassword = await bcrypt.hash(newPassword, saltRounds); - // Update user's password - await prisma.users.update({ - where: { id: tokenRecord.userId }, - data: { password: hashedPassword }, + await prisma.userAuths.updateMany({ + where: { userId: tokenRecord.user.id }, + data: { passwordHash: hashedPassword }, }); - // Delete the used token - await prisma.userToken.update({ - where: { userId: tokenRecord.userId }, + await prisma.userTokens.update({ + where: { id: tokenRecord.id }, data: { resetPasswordToken: null, resetPasswordExpires: null, @@ -173,10 +247,6 @@ class AuthService { } } - - - // Refresh Tokens -> These are tokens use in the background to keep the user logged in without them having to re-enter their credentials. - // They are usually long-lived and can be used to obtain new access tokens when the old ones expire. public static async refreshToken(input: RefreshTokenDTO): Promise { return { success: true, @@ -188,9 +258,6 @@ class AuthService { } } } - - } - export default AuthService; \ No newline at end of file From c6d9da5ae4e2e04b6b9b567f4aa0d4a9e060e0b4 Mon Sep 17 00:00:00 2001 From: Hadiqa Raza Date: Wed, 13 May 2026 13:43:51 +0300 Subject: [PATCH 4/9] feat: implement forgot and reset password functionality --- .env.example | 12 +- package-lock.json | 2879 ++-------------------------------- package.json | 3 + prisma/dev.db | Bin 57344 -> 61440 bytes prisma/schema.prisma | 4 + src/helpers/jwt.ts | 2 +- src/helpers/password.ts | 9 +- src/services/auth.service.ts | 187 +-- 8 files changed, 249 insertions(+), 2847 deletions(-) diff --git a/.env.example b/.env.example index 59137b8..9998c26 100644 --- a/.env.example +++ b/.env.example @@ -1,11 +1,11 @@ -DATABASE_URL= -PORT= +DATABASE_URL="file:./dev.db" +PORT=3000 REDIS_URI_WITH_AUTH= APP_ENV=development -SERVER_TIME_ZONE= +SERVER_TIME_ZONE=Asia/Qatar CORS_ORIGIN=* -RATE_LIMIT_WINDOW= -RATE_LIMIT_MAX= -JWT_SECRET= +RATE_LIMIT_WINDOW=1 minute +RATE_LIMIT_MAX=120 +JWT_SECRET=my_super_secret_key_12345 JWT_ACCESS_EXPIRES_IN=15m JWT_REFRESH_EXPIRES_IN=7d \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 669a84f..c429896 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,6 @@ "@fastify/sensible": "^6.0.0", "@prisma/client": "^5.19.1", "bcrypt": "^6.0.0", - "bcryptjs": "^3.0.3", "chalk": "^5.6.2", "dotenv": "^17.4.2", "fastify": "^5.0.0", @@ -27,17 +26,14 @@ "zod": "^4.3.6" }, "devDependencies": { + "@jest/globals": "^30.4.1", "@types/bcrypt": "^6.0.0", - "@types/bcryptjs": "^2.4.6", - "@types/jest": "^30.0.0", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^25.0.3", "c8": "^11.0.0", "concurrently": "^9.0.0", "fastify-tsconfig": "^3.0.0", - "jest": "^30.4.2", "prisma": "^5.19.1", - "ts-jest": "^29.4.9", "ts-node": "^10.4.0", "tsx": "^4.21.0", "typescript": "~5.9.2" @@ -599,40 +595,6 @@ "node": ">=12" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", @@ -1284,109 +1246,6 @@ "vary": "^1.1.2" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1470,132 +1329,6 @@ "node": ">=8" } }, - "node_modules/@jest/console": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.4.1.tgz", - "integrity": "sha512-v3bhyxUh9Hgmo5p6hAOXe14/R3ZxZDOsvHleh4B07z3m/x4/ngPUXEm9XwK4sF4u+f+P2ORb0Ge+MgpaqRMVDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.4.1", - "@types/node": "*", - "chalk": "^4.1.2", - "jest-message-util": "30.4.1", - "jest-util": "30.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/console/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/console/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/core": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.4.2.tgz", - "integrity": "sha512-TZJA6cPJUFxoWhxaLo8t0VX/MZX2wPWr0uIDvLSHIvN4gu9h02vSzqI2kBADG1ExqQlC+cY09xKMSreivvrChQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.4.1", - "@jest/pattern": "30.4.0", - "@jest/reporters": "30.4.1", - "@jest/test-result": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "exit-x": "^0.2.2", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.11", - "jest-changed-files": "30.4.1", - "jest-config": "30.4.2", - "jest-haste-map": "30.4.1", - "jest-message-util": "30.4.1", - "jest-regex-util": "30.4.0", - "jest-resolve": "30.4.1", - "jest-resolve-dependencies": "30.4.2", - "jest-runner": "30.4.2", - "jest-runtime": "30.4.2", - "jest-snapshot": "30.4.1", - "jest-util": "30.4.1", - "jest-validate": "30.4.1", - "jest-watcher": "30.4.1", - "pretty-format": "30.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/core/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/core/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/diff-sequences": { "version": "30.4.0", "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", @@ -1707,85 +1440,36 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/reporters": { + "node_modules/@jest/schemas": { "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.4.1.tgz", - "integrity": "sha512-/SnkPCzEQpUaBH81kjdEdDdo2WZl5hxw+BmLDGWjRkm8o7XlhjwsU36cqwe5PGBE5WYpBvDzRSdXx9rbGuJtNA==", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", + "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", "dev": true, "license": "MIT", "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.4.1", - "@jest/test-result": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", - "@jridgewell/trace-mapping": "^0.3.25", - "@types/node": "*", - "chalk": "^4.1.2", - "collect-v8-coverage": "^1.0.2", - "exit-x": "^0.2.2", - "glob": "^10.5.0", - "graceful-fs": "^4.2.11", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^5.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "30.4.1", - "jest-util": "30.4.1", - "jest-worker": "30.4.1", - "slash": "^3.0.0", - "string-length": "^4.0.2", - "v8-to-istanbul": "^9.0.1" + "@sinclair/typebox": "^0.34.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/reporters/node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jest/reporters/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jest/reporters/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jest/reporters/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "node_modules/@jest/snapshot-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.4.1.tgz", + "integrity": "sha512-ObY4ljvQ95mt6iwKtVLetR/4yXiAgl3H4nJxhztr0MTjrN97TwDYrnCp/kF60Ec9HdhkWTHSu+Hg05aXfngpOA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "@jest/types": "30.4.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/reporters/node_modules/chalk": { + "node_modules/@jest/snapshot-utils/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", @@ -1802,204 +1486,25 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/@jest/reporters/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "node_modules/@jest/snapshot-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" + "has-flag": "^4.0.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": ">=8" } }, - "node_modules/@jest/reporters/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/@jest/reporters/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "node_modules/@jest/transform": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.4.1.tgz", + "integrity": "sha512-Wz0LyktlTvRefoymh+n64hQ84KNXsRGcwdoZ8CSa0Ea+fgYcHZlnk+hDP7v2MS7il2bQ5uTEIxf4/NNfhMN4KQ==", "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@jest/reporters/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@jest/reporters/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/schemas": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", - "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/snapshot-utils": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.4.1.tgz", - "integrity": "sha512-ObY4ljvQ95mt6iwKtVLetR/4yXiAgl3H4nJxhztr0MTjrN97TwDYrnCp/kF60Ec9HdhkWTHSu+Hg05aXfngpOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.4.1", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "natural-compare": "^1.4.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/snapshot-utils/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/snapshot-utils/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/source-map": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", - "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "callsites": "^3.1.0", - "graceful-fs": "^4.2.11" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/source-map/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@jest/test-result": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.4.1.tgz", - "integrity": "sha512-/ZG7pgEiOmmWkN9TplKbOu4id2N5lh7FHwRwlkgBVAzGdRH+OkkQ8wX/kIxg4zmd3ZQvAL1RwL2yWsvNYYECTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.4.1", - "@jest/types": "30.4.1", - "@types/istanbul-lib-coverage": "^2.0.6", - "collect-v8-coverage": "^1.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.4.1.tgz", - "integrity": "sha512-PeYE+4td5rKjoRPxztObrXU+H8hsjZfxKMXOcmrr34JerSyB/ROOxbbicz8B7A5j9R9VayDnVPvBmedqCsFCdw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "30.4.1", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.4.1.tgz", - "integrity": "sha512-Wz0LyktlTvRefoymh+n64hQ84KNXsRGcwdoZ8CSa0Ea+fgYcHZlnk+hDP7v2MS7il2bQ5uTEIxf4/NNfhMN4KQ==", - "dev": true, - "license": "MIT", + "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", "@jest/types": "30.4.1", @@ -2191,36 +1696,12 @@ "node": ">=8" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" - } - }, "node_modules/@pinojs/redact": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", "license": "MIT" }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@pkgr/core": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", @@ -2429,62 +1910,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", - "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } - }, "node_modules/@types/bcrypt": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", @@ -2495,13 +1920,6 @@ "@types/node": "*" } }, - "node_modules/@types/bcryptjs": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", - "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -2529,17 +1947,6 @@ "@types/istanbul-lib-report": "*" } }, - "node_modules/@types/jest": { - "version": "30.0.0", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", - "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^30.0.0", - "pretty-format": "^30.0.0" - } - }, "node_modules/@types/jsonwebtoken": { "version": "9.0.10", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", @@ -2599,351 +2006,58 @@ "dev": true, "license": "ISC" }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", - "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", - "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", - "cpu": [ - "arm64" - ], + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", - "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", - "cpu": [ - "arm64" - ], + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", - "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", - "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", - "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", - "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", - "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", - "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", - "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", - "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", - "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", - "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", - "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", - "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", - "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^0.2.11" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", - "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", - "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", - "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/abstract-logging": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", - "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", - "license": "MIT" - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "license": "MIT", "dependencies": { "ajv": "^8.0.0" @@ -2957,22 +2071,6 @@ } } }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -3071,58 +2169,6 @@ "fastq": "^1.17.1" } }, - "node_modules/babel-jest": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.4.1.tgz", - "integrity": "sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/transform": "30.4.1", - "@types/babel__core": "^7.20.5", - "babel-plugin-istanbul": "^7.0.1", - "babel-preset-jest": "30.4.0", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0 || ^8.0.0-0" - } - }, - "node_modules/babel-jest/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/babel-jest/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/babel-plugin-istanbul": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", @@ -3211,19 +2257,6 @@ "node": ">=8" } }, - "node_modules/babel-plugin-jest-hoist": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.4.0.tgz", - "integrity": "sha512-9EdtWM/sSfXLOGLwSn+GS6pIXyBnL07/8gyJlwFXjWy4DxMOyItqyUT29d4lQiS380EZwYlX7/At4PgBS+m2aA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/babel__core": "^7.20.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, "node_modules/babel-preset-current-node-syntax": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", @@ -3251,23 +2284,6 @@ "@babel/core": "^7.0.0 || ^8.0.0-0" } }, - "node_modules/babel-preset-jest": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.4.0.tgz", - "integrity": "sha512-lBY4jxsNmCnSiu7kquw8ZC9F4+XLMOKypT3RnNHPvU2Kpd4W0xaPuLr5ZkRyOsvLYAY4yaW1ZwTW4xB7NIiZzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "30.4.0", - "babel-preset-current-node-syntax": "^1.2.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0 || ^8.0.0-beta.1" - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -3305,15 +2321,6 @@ "node": ">= 18" } }, - "node_modules/bcryptjs": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", - "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", - "license": "BSD-3-Clause", - "bin": { - "bcrypt": "bin/bcrypt" - } - }, "node_modules/brace-expansion": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", @@ -3361,19 +2368,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-json-stable-stringify": "2.x" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -3390,13 +2384,6 @@ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "license": "BSD-3-Clause" }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, "node_modules/c8": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/c8/-/c8-11.0.0.tgz", @@ -3431,16 +2418,6 @@ } } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -3484,16 +2461,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/chokidar": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", @@ -3525,13 +2492,6 @@ "node": ">=8" } }, - "node_modules/cjs-module-lexer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", - "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", - "dev": true, - "license": "MIT" - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -3562,24 +2522,6 @@ "node": ">=0.10.0" } }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", - "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", - "dev": true, - "license": "MIT" - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3758,31 +2700,6 @@ } } }, - "node_modules/dedent": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", - "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -3801,16 +2718,6 @@ "node": ">=6" } }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/diff": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", @@ -3833,13 +2740,6 @@ "url": "https://dotenvx.com" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -3856,19 +2756,6 @@ "dev": true, "license": "ISC" }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -3885,16 +2772,6 @@ "once": "^1.4.0" } }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/esbuild": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", @@ -3971,47 +2848,6 @@ "node": ">=4" } }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/execa/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/exit-x": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", - "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/expect": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/expect/-/expect-30.4.1.tgz", @@ -4404,19 +3240,6 @@ "node": ">=8.0.0" } }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-tsconfig": { "version": "4.14.0", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", @@ -4454,28 +3277,6 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, - "node_modules/handlebars": { - "version": "4.7.9", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", - "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -4518,36 +3319,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -4585,13 +3356,6 @@ "node": ">= 10" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, "node_modules/is-docker": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", @@ -4617,29 +3381,6 @@ "node": ">=8" } }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/isbinaryfile": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", @@ -4714,32 +3455,6 @@ "node": ">=8" } }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-lib-source-maps/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, "node_modules/istanbul-reports": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", @@ -4754,702 +3469,23 @@ "node": ">=8" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/jest": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.4.2.tgz", - "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "30.4.2", - "@jest/types": "30.4.1", - "import-local": "^3.2.0", - "jest-cli": "30.4.2" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-changed-files": { + "node_modules/jest-diff": { "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.4.1.tgz", - "integrity": "sha512-IuctmYrxi21iOSOaIXpJWalHyPAsVv0GeBHKDn8C1CA4W5htHn7INL+wdnL4Bo0+olEndvAFkmb++tIQJG+vvg==", - "dev": true, - "license": "MIT", - "dependencies": { - "execa": "^5.1.1", - "jest-util": "30.4.1", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-circus": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.4.2.tgz", - "integrity": "sha512-rvHH7VlY6LgbJXJTQ87GW62g1FntOtbhh0zT+v04kC+pgL6aBKyYINXxWukCpj3dcIBMw5/XUbtDS9dU9JTXeQ==", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", + "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.4.1", - "@jest/expect": "30.4.1", - "@jest/test-result": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", + "@jest/diff-sequences": "30.4.0", + "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "co": "^4.6.0", - "dedent": "^1.6.0", - "is-generator-fn": "^2.1.0", - "jest-each": "30.4.1", - "jest-matcher-utils": "30.4.1", - "jest-message-util": "30.4.1", - "jest-runtime": "30.4.2", - "jest-snapshot": "30.4.1", - "jest-util": "30.4.1", - "p-limit": "^3.1.0", - "pretty-format": "30.4.1", - "pure-rand": "^7.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-circus/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-circus/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-cli": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.4.2.tgz", - "integrity": "sha512-jfA2ocvVHMXS2QijrJ0d31ektP+d/W0T5RpcTX2Pq+3sVqHlsXVCM2+FmwpL+bdY8OfHpIg9xMxLF17Zg0U49Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "30.4.2", - "@jest/test-result": "30.4.1", - "@jest/types": "30.4.1", - "chalk": "^4.1.2", - "exit-x": "^0.2.2", - "import-local": "^3.2.0", - "jest-config": "30.4.2", - "jest-util": "30.4.1", - "jest-validate": "30.4.1", - "yargs": "^17.7.2" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-cli/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-cli/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-config": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.4.2.tgz", - "integrity": "sha512-rNHAShJQqQwFNoL0hbf3BphSBOWnpOUAKvidLS/AjNVLPfoj5mSf4jQMfW3cYOs6hXeZC7nF7mDHaBnbxELOzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/get-type": "30.1.0", - "@jest/pattern": "30.4.0", - "@jest/test-sequencer": "30.4.1", - "@jest/types": "30.4.1", - "babel-jest": "30.4.1", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "deepmerge": "^4.3.1", - "glob": "^10.5.0", - "graceful-fs": "^4.2.11", - "jest-circus": "30.4.2", - "jest-docblock": "30.4.0", - "jest-environment-node": "30.4.1", - "jest-regex-util": "30.4.0", - "jest-resolve": "30.4.1", - "jest-runner": "30.4.2", - "jest-util": "30.4.1", - "jest-validate": "30.4.1", - "parse-json": "^5.2.0", - "pretty-format": "30.4.1", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "esbuild-register": ">=3.4.0", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "esbuild-register": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/jest-config/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-config/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/jest-config/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-config/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/jest-config/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/jest-config/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/jest-config/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/jest-config/node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-config/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-diff": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", - "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/diff-sequences": "30.4.0", - "@jest/get-type": "30.1.0", - "chalk": "^4.1.2", - "pretty-format": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-diff/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-docblock": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.4.0.tgz", - "integrity": "sha512-ZPMabUZCx5MpbZ2eBYSvZ0J8fvo3dR9oM+eeUpb3aKNQFuS2tu3Duw1TNlMoP8k3WQgKGJuhcMFvwcVuq6T7oA==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-newline": "^3.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-each": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.4.1.tgz", - "integrity": "sha512-/8MJbH6fuj48TstjrMf+u/pd06Qezz5xOXvZA6442heNOWr8bdeoGZX2d9fCn028CoMgYmroH9//zky5GfyYmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0", - "@jest/types": "30.4.1", - "chalk": "^4.1.2", - "jest-util": "30.4.1", - "pretty-format": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-each/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-each/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-environment-node": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.4.1.tgz", - "integrity": "sha512-4FZYVOk85hz2AyT6BbarKy9u37g6DbrDyCdFhsnDdXqyrueYQvB+0zO4f/kqLCRD0BsPRXPMNJeQwihKZV8naw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.4.1", - "@jest/fake-timers": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", - "jest-mock": "30.4.1", - "jest-util": "30.4.1", - "jest-validate": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-haste-map": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.4.1.tgz", - "integrity": "sha512-rFrcONd8jeFsyw+Z9CrScJgglRf2+NFmNam8dKu7n+SoHqNYT47mn0DdEcVUZJpvh7Iz6/si7f7yUH7GJHVgnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.4.1", - "@types/node": "*", - "anymatch": "^3.1.3", - "fb-watchman": "^2.0.2", - "graceful-fs": "^4.2.11", - "jest-regex-util": "30.4.0", - "jest-util": "30.4.1", - "jest-worker": "30.4.1", - "picomatch": "^4.0.3", - "walker": "^1.0.8" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.3" - } - }, - "node_modules/jest-leak-detector": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.4.1.tgz", - "integrity": "sha512-IpmyiioeHxiWDhesHnUFmOxcTzwCwKpgACgWajtAP+nYQXiY7DakTxB6Bx9JFiRMljr0AX1PvnQdaU1KFoz6NQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0", "pretty-format": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-matcher-utils": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz", - "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0", - "chalk": "^4.1.2", - "jest-diff": "30.4.1", - "pretty-format": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-message-util": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", - "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.4.1", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "jest-util": "30.4.1", - "picomatch": "^4.0.3", - "pretty-format": "30.4.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-message-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-mock": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", - "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.4.1", - "@types/node": "*", - "jest-util": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" - }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", - "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-resolve": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.4.1.tgz", - "integrity": "sha512-Zry8Yq/yJcNAZ7dJ5F2heic8AheXvbFZ7XI5V+h28nrYZ7Qoyy4dItq8OodjnYD270mvX+ZudmrNV9cysqhW5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.4.1", - "jest-pnp-resolver": "^1.2.3", - "jest-util": "30.4.1", - "jest-validate": "30.4.1", - "slash": "^3.0.0", - "unrs-resolver": "^1.7.11" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-resolve-dependencies": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.4.2.tgz", - "integrity": "sha512-gDiVh1I+GxYzz9oXlyw+1wv6VOYX1WYxMOfjsA3iGKePV2oxmbHhwxfkALxNxYy1ciw6APWwkW2zZONwP97aEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-regex-util": "30.4.0", - "jest-snapshot": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-resolve/node_modules/chalk": { + "node_modules/jest-diff/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", @@ -5466,7 +3502,7 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/jest-resolve/node_modules/supports-color": { + "node_modules/jest-diff/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", @@ -5479,41 +3515,48 @@ "node": ">=8" } }, - "node_modules/jest-runner": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.4.2.tgz", - "integrity": "sha512-2dw0PslVYXxffXGpLo+Ejad+KcI1Qkjn7f4X4619gf21oCUmL+SPfjqIa/losUem3yEOvfNZe/F1HWUcNpODcg==", + "node_modules/jest-haste-map": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.4.1.tgz", + "integrity": "sha512-rFrcONd8jeFsyw+Z9CrScJgglRf2+NFmNam8dKu7n+SoHqNYT47mn0DdEcVUZJpvh7Iz6/si7f7yUH7GJHVgnw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.4.1", - "@jest/environment": "30.4.1", - "@jest/test-result": "30.4.1", - "@jest/transform": "30.4.1", "@jest/types": "30.4.1", "@types/node": "*", - "chalk": "^4.1.2", - "emittery": "^0.13.1", - "exit-x": "^0.2.2", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", "graceful-fs": "^4.2.11", - "jest-docblock": "30.4.0", - "jest-environment-node": "30.4.1", - "jest-haste-map": "30.4.1", - "jest-leak-detector": "30.4.1", - "jest-message-util": "30.4.1", - "jest-resolve": "30.4.1", - "jest-runtime": "30.4.2", + "jest-regex-util": "30.4.0", "jest-util": "30.4.1", - "jest-watcher": "30.4.1", "jest-worker": "30.4.1", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" + "picomatch": "^4.0.3", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz", + "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.4.1", + "pretty-format": "30.4.1" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-runner/node_modules/chalk": { + "node_modules/jest-matcher-utils/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", @@ -5530,7 +3573,7 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/jest-runner/node_modules/supports-color": { + "node_modules/jest-matcher-utils/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", @@ -5543,58 +3586,29 @@ "node": ">=8" } }, - "node_modules/jest-runtime": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.4.2.tgz", - "integrity": "sha512-3/5e8iPz2k/VLqlr8DgTftYyLUv8Su3FkCAO2/Od81UsUTpSxOrS6O5x5KkoQwyUjmpYyDJKeyAvg2T2nvpNkQ==", + "node_modules/jest-message-util": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", + "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.4.1", - "@jest/fake-timers": "30.4.1", - "@jest/globals": "30.4.1", - "@jest/source-map": "30.0.1", - "@jest/test-result": "30.4.1", - "@jest/transform": "30.4.1", + "@babel/code-frame": "^7.27.1", "@jest/types": "30.4.1", - "@types/node": "*", + "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", - "cjs-module-lexer": "^2.1.0", - "collect-v8-coverage": "^1.0.2", - "glob": "^10.5.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.4.1", - "jest-message-util": "30.4.1", - "jest-mock": "30.4.1", - "jest-regex-util": "30.4.0", - "jest-resolve": "30.4.1", - "jest-snapshot": "30.4.1", "jest-util": "30.4.1", + "picomatch": "^4.0.3", + "pretty-format": "30.4.1", "slash": "^3.0.0", - "strip-bom": "^4.0.0" + "stack-utils": "^2.0.6" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-runtime/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-runtime/node_modules/brace-expansion": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", - "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/jest-runtime/node_modules/chalk": { + "node_modules/jest-message-util/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", @@ -5611,79 +3625,42 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/jest-runtime/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/jest-runtime/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/jest-runtime/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "node_modules/jest-message-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^2.0.2" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=8" } }, - "node_modules/jest-runtime/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "node_modules/jest-mock": { + "version": "30.4.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", + "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "@jest/types": "30.4.1", + "@types/node": "*", + "jest-util": "30.4.1" }, "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-runtime/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/jest-regex-util": { + "version": "30.4.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", + "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", "dev": true, "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-snapshot": { @@ -5797,117 +3774,6 @@ "node": ">=8" } }, - "node_modules/jest-validate": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.4.1.tgz", - "integrity": "sha512-PDWi4SOwLnwqNDfHZjOcsEFyZ4fc/2W2gVL3DEoyqnB6jCQMLRtfBong8s6omIw3lI0HWOus12xfnFmQtjW3fw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0", - "@jest/types": "30.4.1", - "camelcase": "^6.3.0", - "chalk": "^4.1.2", - "leven": "^3.1.0", - "pretty-format": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-validate/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-validate/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watcher": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.4.1.tgz", - "integrity": "sha512-/l9UonmvCwjHH7d2h3iAwIloLc1H0S8mJZ/LNK3i86hqwPAz8otUJjP9MfYtz9Tt77Su5FD2xGjZn8d31IZHlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "emittery": "^0.13.1", - "jest-util": "30.4.1", - "string-length": "^4.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-watcher/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-watcher/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-worker": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.4.1.tgz", @@ -5968,13 +3834,6 @@ "node": ">=6" } }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -6065,16 +3924,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/light-my-request": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", @@ -6112,13 +3961,6 @@ ], "license": "MIT" }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -6171,13 +4013,6 @@ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "license": "MIT" }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -6283,16 +4118,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", @@ -6334,22 +4159,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/napi-postinstall": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", - "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", - "dev": true, - "license": "MIT", - "bin": { - "napi-postinstall": "lib/cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/napi-postinstall" - } - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -6357,13 +4166,6 @@ "dev": true, "license": "MIT" }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, "node_modules/node-addon-api": { "version": "8.7.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz", @@ -6400,25 +4202,12 @@ }, "node_modules/normalize-path": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, "node_modules/on-exit-leak-free": { @@ -6439,22 +4228,6 @@ "wrappy": "1" } }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -6496,32 +4269,6 @@ "node": ">=6" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -6669,75 +4416,6 @@ "node": ">= 6" } }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/pkg-up": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", @@ -6886,23 +4564,6 @@ "once": "^1.3.1" } }, - "node_modules/pure-rand": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", - "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, "node_modules/quick-format-unescaped": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", @@ -6996,19 +4657,6 @@ "node": ">=0.10.0" } }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -7222,27 +4870,6 @@ "atomic-sleep": "^1.0.0" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/split2": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", @@ -7304,20 +4931,6 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -7333,22 +4946,6 @@ "node": ">=8" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -7362,40 +4959,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/strip-json-comments": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", @@ -7501,72 +5064,6 @@ "tree-kill": "cli.js" } }, - "node_modules/ts-jest": { - "version": "29.4.9", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.9.tgz", - "integrity": "sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bs-logger": "^0.2.6", - "fast-json-stable-stringify": "^2.1.0", - "handlebars": "^4.7.9", - "json5": "^2.2.3", - "lodash.memoize": "^4.1.2", - "make-error": "^1.3.6", - "semver": "^7.7.4", - "type-fest": "^4.41.0", - "yargs-parser": "^21.1.1" - }, - "bin": { - "ts-jest": "cli.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" - }, - "peerDependencies": { - "@babel/core": ">=7.0.0-beta.0 <8", - "@jest/transform": "^29.0.0 || ^30.0.0", - "@jest/types": "^29.0.0 || ^30.0.0", - "babel-jest": "^29.0.0 || ^30.0.0", - "jest": "^29.0.0 || ^30.0.0", - "jest-util": "^29.0.0 || ^30.0.0", - "typescript": ">=4.3 <7" - }, - "peerDependenciesMeta": { - "@babel/core": { - "optional": true - }, - "@jest/transform": { - "optional": true - }, - "@jest/types": { - "optional": true - }, - "babel-jest": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jest-util": { - "optional": true - } - } - }, - "node_modules/ts-jest/node_modules/type-fest": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", - "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -7648,19 +5145,6 @@ "node": ">=4" } }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -7689,20 +5173,6 @@ "node": ">=14.17" } }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/undici-types": { "version": "7.19.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", @@ -7719,41 +5189,6 @@ "node": ">= 4.0.0" } }, - "node_modules/unrs-resolver": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", - "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "napi-postinstall": "^0.3.0" - }, - "funding": { - "url": "https://opencollective.com/unrs-resolver" - }, - "optionalDependencies": { - "@unrs/resolver-binding-android-arm-eabi": "1.11.1", - "@unrs/resolver-binding-android-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-arm64": "1.11.1", - "@unrs/resolver-binding-darwin-x64": "1.11.1", - "@unrs/resolver-binding-freebsd-x64": "1.11.1", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", - "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", - "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", - "@unrs/resolver-binding-linux-x64-musl": "1.11.1", - "@unrs/resolver-binding-wasm32-wasi": "1.11.1", - "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", - "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", - "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" - } - }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -7858,13 +5293,6 @@ "node": ">= 8" } }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true, - "license": "MIT" - }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -7883,25 +5311,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 6513816..ce8fd50 100644 --- a/package.json +++ b/package.json @@ -28,12 +28,15 @@ "fastify": "^5.0.0", "fastify-cli": "^8.0.0", "fastify-plugin": "^5.0.0", + "jsonwebtoken": "^9.0.3", "log4js": "^6.9.1", "redis": "^5.12.1", "zod": "^4.3.6" }, "devDependencies": { + "@jest/globals": "^30.4.1", "@types/bcrypt": "^6.0.0", + "@types/jsonwebtoken": "^9.0.10", "@types/node": "^25.0.3", "c8": "^11.0.0", "concurrently": "^9.0.0", diff --git a/prisma/dev.db b/prisma/dev.db index 29a9b36b28e9dd17e17841122bece9ef78f0a12d..3faa8c912c7cedc434feb377758f100261124e13 100644 GIT binary patch delta 1821 zcmcIkUuauZ7{AHwP5&e}C)Kr?bkMmL>eQ3v{!PN5+9qvH+9Yf2ZJJ0Kx&PKAO`A4J z+vLHnv&}a2VclgY8|=X#3RAighQ17o51oR}2T_Dg=7adK&AtetFU514u^|=~AH0|E zZQDS^cfH+P*VtZ{T#xTPDzG@98nD4QmSCX=v2e@UnKdc~;xC zq^j-21EM{PZP`0J4Qt+fLY!7iz=Y5+OtYJ3P2f+%{eezxlRTt-WNszV=cnjb%4V`Z z-nDgsGbIG^B!iGs=KBiysodj5qjtx9<`HNHKOXEs84qe@@{j>kuaU7MR)XBYz`MX< z-L_7UJBa*UlA9vT0&Hs|2<-VTjF(wa}{(~^nPrx5`e|PkAMY}pbRz;z!7lt1za%Hlb5JYhAXy<*#;|N z*~Nfr{n|>C?^$+HmWP(*6+6)=2rC3Zh$`z+Wl4p!#K?#vE5d|RlAywKj6&0*Ta^&i zr`ORgoG(S zGc&U=FjOlX`G@y!uh<**sA-9B$_LQa_JuEzzjbzO#g5kk=G%LL$}I~DeDL=7D|TZo zaHAG@=grSh;L?ls6}zDpIQeb)Alf>2K3Vf5l8O;nF>n@GQA3PidC}M;2+zHQ8sd^T zbT}#vIXCZttW$Er1nr@rn`b0hlquE2yX$F)i}O&tTR&bXD+@_UffUUk_&+ZHh5T>1 z<2Ao%(1DkAWKN|$qDNJshjmMkWm$yGF&uQus_3LCMwVDtJ;)5>qYE+^C?$S`{^nh zqT{|uGL{nPV3JCPqQcV1nEoBihkbq~R-`g+Db}3YnxL`^ug-WLwb<`n^7nSk; z@MzX`VqVCP4p345TrSs_d@XlUVA6%*Aue4Q9PtO(RQi8nxV#TTHk68SVc#A@=bWD# zU5rS9*I5`I8qa&hJmbuG)M7XvX9|(Ngm-Q}mg);d27>=qUv0ka{1RRR4Ygx~s}LQ& zi)9q2Dlw3wt2ZwVwbZ6wSyv^Rd+d{`SC2{vZF1v7l;H{nz5+Yo3b+pLAZ?!qZ3l2@ z&`G_jEG078X~iMUW{B$b&rjHFNTm8>M5YVYgdl@Jo4i+fM!zDDhVTX&!T+FB&)`IK H?p6KQb!7&nm1C3hlMYKfqy!`9^Wy(2w<3b z@NJH;Tf?}SCEySLq5y$S0W1Pc3<8@46^`>yTwu(`1=0xT3h)AXEUdt2Fz4FJVaf^$ zbRSmE)-V>{(9lL+*2$}<+HGDuHIa*lg+G>ozm5L}|9bvdpdw%X$?N-BCnv36#KFS9 kfr0-d|GUk68+w6Oh;QDoKR`eLWDi&mNJkq?Pwf5003KAD?f?J) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9c7fd4e..3ce1d08 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -18,6 +18,10 @@ model Users { email String @unique firstName String? lastName String? + phone String? // ← ADD THIS + company String? // ← ADD THIS + profilePicture String? // ← ADD THIS + bio String? // ← ADD THIS createdAt DateTime @default(now()) updatedAt DateTime @updatedAt UserAuths UserAuths[] diff --git a/src/helpers/jwt.ts b/src/helpers/jwt.ts index 13767d8..324908f 100644 --- a/src/helpers/jwt.ts +++ b/src/helpers/jwt.ts @@ -37,7 +37,7 @@ export function generateJwtToken(payload: TokenPayload): string { export function verifyToken(token: string): TokenPayload { if (!JWT_SECRET) { - throw new Error('JWT_SECRET is not defined in config'); + throw new Error('JWT_SECRET is not defined'); } return jwt.verify(token, JWT_SECRET) as TokenPayload; } \ No newline at end of file diff --git a/src/helpers/password.ts b/src/helpers/password.ts index 64b64e2..9c482cc 100644 --- a/src/helpers/password.ts +++ b/src/helpers/password.ts @@ -1,11 +1,10 @@ -import bcrypt from 'bcryptjs'; - -const SALT_ROUNDS = 12; +import bcrypt from 'bcrypt'; export async function hashPassword(password: string): Promise { - return bcrypt.hash(password, SALT_ROUNDS); + const saltRounds = 10; + return bcrypt.hash(password, saltRounds); } export async function verifyPassword(password: string, hash: string): Promise { return bcrypt.compare(password, hash); -} +} \ No newline at end of file diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 3f4649a..ace746f 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -21,101 +21,27 @@ class AuthService { } public static async signup(input: SignupDTO): Promise { - const { email, password, firstName, lastName, phone, company, deviceId } = input; + const { email, password, firstName, lastName, phone, company, deviceId } = input; - const existingUser = await prisma.users.findUnique({ where: { email } }); - if (existingUser) { - throw new BadRequestError({ - msg: "Account with the email already exists", - errorCode: CustomErrorCode.DUPLICATE_RESOURCE - }); - } - - const passwordHash = await hashPassword(password); - - const { user, accessToken, refreshToken } = await prisma.$transaction(async (tx) => { - const user = await tx.users.create({ - data: { email, firstName, lastName, phone, company }, - }); - - await tx.userAuths.create({ - data: { userId: user.id, passwordHash, recognisedDevices: deviceId }, - }); - - const accessToken = generateJwtToken({ - userId: user.id, - email: user.email, - deviceId, - tokenType: TOKEN_TYPE.AUTH_TOKEN - }); - const refreshToken = generateJwtToken({ - userId: user.id, - email: user.email, - deviceId, - tokenType: TOKEN_TYPE.REFRESH_TOKEN - }); - - await tx.userTokens.create({ - data: { userId: user.id, deviceId, accessToken, refreshToken }, - }); - - return { user, accessToken, refreshToken }; + const existingUser = await prisma.users.findUnique({ where: { email } }); + if (existingUser) { + throw new BadRequestError({ + msg: "Account with the email already exists", + errorCode: CustomErrorCode.DUPLICATE_RESOURCE }); - - return { - success: true, - message: "Signup successful", - data: { - accessToken, - refreshToken, - user, - }, - }; } - public static async login(input: LoginDTO): Promise { - const { email, password, deviceId } = input; - - const user = await prisma.users.findUnique({ where: { email } }); - if (!user) { - throw new UnAuthorizedError({ - msg: "Invalid email or password", - errorCode: CustomErrorCode.AUTH_INVALID, - }); - } + const passwordHash = await hashPassword(password); - const userAuth = await prisma.userAuths.findFirst({ - where: { userId: user.id }, + const { user, accessToken, refreshToken } = await prisma.$transaction(async (tx) => { + const user = await tx.users.create({ + data: { email, firstName, lastName, phone, company }, }); - if (!userAuth) { - throw new UnAuthorizedError({ - msg: "Invalid email or password", - errorCode: CustomErrorCode.AUTH_INVALID, - }); - } - - const passwordMatch = await verifyPassword(password, userAuth.passwordHash); - if (!passwordMatch) { - throw new UnAuthorizedError({ - msg: "Invalid email or password", - errorCode: CustomErrorCode.AUTH_INVALID, - }); - } - - const isRecognisedDevice = userAuth.recognisedDevices === deviceId; - if (!isRecognisedDevice) { - const otp = Math.floor(100000 + Math.random() * 900000).toString(); - const expiresAt = new Date(Date.now() + 10 * 60 * 1000); - - await prisma.userVerifications.create({ - data: { userId: user.id, token: otp, deviceId, expiresAt }, - }); - throw new UnAuthorizedError({ - msg: "Unrecognised device. A verification code has been sent to your email.", - errorCode: CustomErrorCode.AUTH_BLOCKED, - }); - } + // REMOVED recognisedDevices from here + await tx.userAuths.create({ + data: { userId: user.id, passwordHash }, + }); const accessToken = generateJwtToken({ userId: user.id, @@ -130,22 +56,83 @@ class AuthService { tokenType: TOKEN_TYPE.REFRESH_TOKEN }); - await prisma.userTokens.updateMany({ - where: { userId: user.id, deviceId }, - data: { accessToken, refreshToken }, + await tx.userTokens.create({ + data: { userId: user.id, deviceId, accessToken, refreshToken }, }); - return { - success: true, - message: "Login successful", - data: { - accessToken, - refreshToken, - user, - }, - }; + return { user, accessToken, refreshToken }; + }); + + return { + success: true, + message: "Signup successful", + data: { + accessToken, + refreshToken, + user, + }, + }; +} + + public static async login(input: LoginDTO): Promise { + const { email, password, deviceId } = input; + + const user = await prisma.users.findUnique({ where: { email } }); + if (!user) { + throw new UnAuthorizedError({ + msg: "Invalid email or password", + errorCode: CustomErrorCode.AUTH_INVALID, + }); + } + + const userAuth = await prisma.userAuths.findFirst({ + where: { userId: user.id }, + }); + if (!userAuth) { + throw new UnAuthorizedError({ + msg: "Invalid email or password", + errorCode: CustomErrorCode.AUTH_INVALID, + }); } + const passwordMatch = await verifyPassword(password, userAuth.passwordHash); + if (!passwordMatch) { + throw new UnAuthorizedError({ + msg: "Invalid email or password", + errorCode: CustomErrorCode.AUTH_INVALID, + }); + } + + + const accessToken = generateJwtToken({ + userId: user.id, + email: user.email, + deviceId, + tokenType: TOKEN_TYPE.AUTH_TOKEN + }); + const refreshToken = generateJwtToken({ + userId: user.id, + email: user.email, + deviceId, + tokenType: TOKEN_TYPE.REFRESH_TOKEN + }); + + await prisma.userTokens.updateMany({ + where: { userId: user.id, deviceId }, + data: { accessToken, refreshToken }, + }); + + return { + success: true, + message: "Login successful", + data: { + accessToken, + refreshToken, + user, + }, + }; +} + public static async verifyDeviceChange(input: VerifyDeviceChangeOTPDTO): Promise { return { success: true, From 705e7c9b16e22da5b935095d8ae5d10787d4542e Mon Sep 17 00:00:00 2001 From: Hadiqa Raza Date: Sun, 31 May 2026 10:50:04 +0300 Subject: [PATCH 5/9] fix: address review feedback - revert to PostgreSQL, move resetPasswordToken, rename authToken, fix email validation --- prisma/schema.prisma | 12 ++++++------ src/interfaces/auth.interface.ts | 2 +- src/schemas/auth.schema.ts | 4 ++-- src/services/auth.service.ts | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a397982..b0b1021 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -3,7 +3,7 @@ generator client { } datasource db { - provider = "sqlite" + provider = "postgresql" url = "file:./dev.db" } @@ -12,10 +12,10 @@ model Users { email String @unique firstName String? lastName String? - phone String? // ← ADD THIS - company String? // ← ADD THIS - profilePicture String? // ← ADD THIS - bio String? // ← ADD THIS + phone String? + company String? + profilePicture String? + bio String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt UserAuths UserAuths[] @@ -36,6 +36,7 @@ model UserVerifications { id String @id @default(uuid()) userId String token String + resetPasswordToken String? deviceId String expiresAt DateTime createdAt DateTime @default(now()) @@ -46,7 +47,6 @@ model UserVerifications { model UserTokens { id String @id @default(uuid()) userId String - resetPasswordToken String? resetPasswordExpires DateTime? refreshToken String accessToken String diff --git a/src/interfaces/auth.interface.ts b/src/interfaces/auth.interface.ts index 2dee147..18eca09 100644 --- a/src/interfaces/auth.interface.ts +++ b/src/interfaces/auth.interface.ts @@ -23,7 +23,7 @@ export interface RefreshTokenDTO extends DeviceId { } export interface ResetPasswordDTO extends DeviceId{ - token: string; + confirmationToken: string; newPassword: string; } diff --git a/src/schemas/auth.schema.ts b/src/schemas/auth.schema.ts index d3fcd27..e08d44d 100644 --- a/src/schemas/auth.schema.ts +++ b/src/schemas/auth.schema.ts @@ -14,11 +14,11 @@ export const LoginRequest = z.object({ }); export const ForgotPasswordRequest = z.object({ - email: z.string().email().min(4).max(255), + email: z.string().email().max(255), }); export const ResetPasswordRequest = z.object({ - token: z.string().min(32), + confirmationToken: z.string().min(32), newPassword: z.string().min(8).max(128), }); export const VerifyDeviceChangeRequest = z.object({ diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 339e7fe..2ff3138 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -238,11 +238,11 @@ class AuthService { } public static async resetPassword(input: ResetPasswordDTO): Promise { - const { token, newPassword, deviceId } = input; + const { confirmationToken, newPassword, deviceId } = input; const tokenRecord = await prisma.userTokens.findFirst({ where: { - resetPasswordToken: token, + resetPasswordToken: confirmationToken, resetPasswordExpires: { gt: new Date() }, }, include: { user: true }, From 166303548e0edf9cb12059fbc987e02a77d14a76 Mon Sep 17 00:00:00 2001 From: Hadiqa Raza Date: Mon, 1 Jun 2026 14:57:05 +0300 Subject: [PATCH 6/9] feat: implement OTP-based password reset - Change database provider to PostgreSQL - Move reset password OTP fields to UserVerifications model - Update ResetPasswordDTO to use OTP instead of token - Remove min length validation from email field - Implement 6-digit OTP generation for forgot password - Update reset password to verify OTP from UserVerifications table - Fix deviceId missing in userVerifications.create --- .gitignore | Bin 859 -> 952 bytes .../20260507135532_init/migration.sql | 48 ----- .../20260512071949_aks/migration.sql | 8 - .../migration.sql | 8 +- .../migration.sql | 2 + prisma/migrations/migration_lock.toml | 2 +- prisma/schema.prisma | 12 +- src/interfaces/auth.interface.ts | 2 +- src/schemas/auth.schema.ts | 2 +- src/services/auth.service.ts | 165 +++++++++--------- 10 files changed, 100 insertions(+), 149 deletions(-) delete mode 100644 prisma/migrations/20260507135532_init/migration.sql delete mode 100644 prisma/migrations/20260512071949_aks/migration.sql rename prisma/migrations/{20260512034815_ => 20260601110845_init}/migration.sql (93%) create mode 100644 prisma/migrations/20260601112355_make_device_id_optional/migration.sql diff --git a/.gitignore b/.gitignore index 95bdd8d019bf3cf05e70ad1da7109254f79b0b7f..125d76963b1f02394d8b8ff840cbfb37268b732b 100644 GIT binary patch literal 952 zcmZ8f%Z}SH4D{K6|3N@5tpVFT_qtgWC>Cgud_W*ejH5&^NOGL~eMh$AUGyS}6geCY zxnm!0h*hcQvu<%$rS+zf)Q>|p>#D5E9XpIRDML*1#H-RPiKM#4ov%oQJBRI6Ady^n zva<_J*_gs;$R?6Zt3I-UECrs;{>iRUxI4^&y5GmQ`~(rzH}FE=G8*<1sw!)cC1ma2Aq49bg(Bm!#XFKtDYtBkj{Ei0Zl$C^&)J*&g685Zy5fUh8 z5u=^BHB%Hp`IXC?Gad*KNLdaYxs=YvjfVviXt*$R+^cT+Z7<1alpn^N-;l{3vi@lG=N;4dLM$=Ldew&i+P$6S?9)an#5 zHFPTYfz*fztC4WHt25br<#5FQ=l(-}fs;B0L1VfK--2?2axdbE*6NmXAZERw^3+Tk zrT|YqDNT2)8d6g)P&0JMQ!7)Ggddg1VT}!IMCVzRB9<=QzMNVs-}-)(lrw3YV{a>( z=F`}fFpWIfnlV^<`1#aVu7~Wx+d~p2^0~$iP6-5>T`xrn{`sAx9jamPC}7il%)AnXyecW4B;@}4*Z{Wv!gb#ty4 z;yD&B8+`Ki*>(e)72uy^FcHUU%4yN^cmEbjyqQ?omA>5zu5Z#2R^CK?CXnq}Z+g$R d-UMSgybSA$nNq;96##vyo}%er^bcU&<3Cl9C;b2b diff --git a/prisma/migrations/20260507135532_init/migration.sql b/prisma/migrations/20260507135532_init/migration.sql deleted file mode 100644 index 0f31bdf..0000000 --- a/prisma/migrations/20260507135532_init/migration.sql +++ /dev/null @@ -1,48 +0,0 @@ --- CreateTable -CREATE TABLE "Users" ( - "id" TEXT NOT NULL PRIMARY KEY, - "email" TEXT NOT NULL, - "firstName" TEXT, - "lastName" TEXT, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL -); - --- CreateTable -CREATE TABLE "UserAuths" ( - "id" TEXT NOT NULL PRIMARY KEY, - "userId" TEXT NOT NULL, - "passwordHash" TEXT NOT NULL, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL, - CONSTRAINT "UserAuths_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE -); - --- CreateTable -CREATE TABLE "UserVerifications" ( - "id" TEXT NOT NULL PRIMARY KEY, - "userId" TEXT NOT NULL, - "token" TEXT NOT NULL, - "deviceId" TEXT NOT NULL, - "expiresAt" DATETIME NOT NULL, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL, - CONSTRAINT "UserVerifications_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE -); - --- CreateTable -CREATE TABLE "UserTokens" ( - "id" TEXT NOT NULL PRIMARY KEY, - "userId" TEXT NOT NULL, - "resetPasswordToken" TEXT, - "resetPasswordExpires" DATETIME, - "refreshToken" TEXT NOT NULL, - "accessToken" TEXT NOT NULL, - "deviceId" TEXT NOT NULL, - "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" DATETIME NOT NULL, - CONSTRAINT "UserTokens_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE -); - --- CreateIndex -CREATE UNIQUE INDEX "Users_email_key" ON "Users"("email"); diff --git a/prisma/migrations/20260512071949_aks/migration.sql b/prisma/migrations/20260512071949_aks/migration.sql deleted file mode 100644 index b6f5b3a..0000000 --- a/prisma/migrations/20260512071949_aks/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - A unique constraint covering the columns `[userId]` on the table `UserTokens` will be added. If there are existing duplicate values, this will fail. - -*/ --- CreateIndex -CREATE UNIQUE INDEX "UserTokens_userId_key" ON "UserTokens"("userId"); diff --git a/prisma/migrations/20260512034815_/migration.sql b/prisma/migrations/20260601110845_init/migration.sql similarity index 93% rename from prisma/migrations/20260512034815_/migration.sql rename to prisma/migrations/20260601110845_init/migration.sql index 9112e11..f7049eb 100644 --- a/prisma/migrations/20260512034815_/migration.sql +++ b/prisma/migrations/20260601110845_init/migration.sql @@ -6,6 +6,8 @@ CREATE TABLE "Users" ( "lastName" TEXT, "phone" TEXT, "company" TEXT, + "profilePicture" TEXT, + "bio" TEXT, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, @@ -17,7 +19,6 @@ CREATE TABLE "UserAuths" ( "id" TEXT NOT NULL, "userId" TEXT NOT NULL, "passwordHash" TEXT NOT NULL, - "recognisedDevices" TEXT[] DEFAULT ARRAY[]::TEXT[], "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, @@ -29,6 +30,8 @@ CREATE TABLE "UserVerifications" ( "id" TEXT NOT NULL, "userId" TEXT NOT NULL, "token" TEXT NOT NULL, + "resetPasswordOTP" TEXT, + "resetPasswordExpires" TIMESTAMP(3), "deviceId" TEXT NOT NULL, "expiresAt" TIMESTAMP(3) NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -53,9 +56,6 @@ CREATE TABLE "UserTokens" ( -- CreateIndex CREATE UNIQUE INDEX "Users_email_key" ON "Users"("email"); --- CreateIndex -CREATE UNIQUE INDEX "UserAuths_userId_key" ON "UserAuths"("userId"); - -- AddForeignKey ALTER TABLE "UserAuths" ADD CONSTRAINT "UserAuths_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20260601112355_make_device_id_optional/migration.sql b/prisma/migrations/20260601112355_make_device_id_optional/migration.sql new file mode 100644 index 0000000..67b51f0 --- /dev/null +++ b/prisma/migrations/20260601112355_make_device_id_optional/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "UserTokens" ALTER COLUMN "deviceId" DROP NOT NULL; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml index 99e4f20..fbffa92 100644 --- a/prisma/migrations/migration_lock.toml +++ b/prisma/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually # It should be added in your version-control system (i.e. Git) -provider = "postgresql" +provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b0b1021..129ff76 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -4,7 +4,7 @@ generator client { datasource db { provider = "postgresql" - url = "file:./dev.db" + url = env("DATABASE_URL") } model Users { @@ -35,8 +35,9 @@ model UserAuths { model UserVerifications { id String @id @default(uuid()) userId String - token String - resetPasswordToken String? + token String // For device verification OTP + resetPasswordOTP String? // For password reset OTP + resetPasswordExpires DateTime? // Expiry for reset OTP deviceId String expiresAt DateTime createdAt DateTime @default(now()) @@ -47,11 +48,10 @@ model UserVerifications { model UserTokens { id String @id @default(uuid()) userId String - resetPasswordExpires DateTime? refreshToken String accessToken String - deviceId String + deviceId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user Users @relation(fields: [userId], references: [id]) -} +} \ No newline at end of file diff --git a/src/interfaces/auth.interface.ts b/src/interfaces/auth.interface.ts index 18eca09..b80b37c 100644 --- a/src/interfaces/auth.interface.ts +++ b/src/interfaces/auth.interface.ts @@ -23,7 +23,7 @@ export interface RefreshTokenDTO extends DeviceId { } export interface ResetPasswordDTO extends DeviceId{ - confirmationToken: string; + otp: string; newPassword: string; } diff --git a/src/schemas/auth.schema.ts b/src/schemas/auth.schema.ts index e08d44d..2f8d752 100644 --- a/src/schemas/auth.schema.ts +++ b/src/schemas/auth.schema.ts @@ -18,7 +18,7 @@ export const ForgotPasswordRequest = z.object({ }); export const ResetPasswordRequest = z.object({ - confirmationToken: z.string().min(32), + otp: z.string().min(6).max(6), // 6-digit OTP newPassword: z.string().min(8).max(128), }); export const VerifyDeviceChangeRequest = z.object({ diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 2ff3138..298507c 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -1,5 +1,5 @@ import crypto from "crypto"; -import bcrypt from "bcrypt"; +//import bcrypt from "bcrypt"; import { ForgotPasswordDTO, IService, @@ -59,7 +59,9 @@ class AuthService { }); await tx.userTokens.create({ - data: { userId: user.id, deviceId, accessToken, refreshToken }, + data: { userId: user.id, + deviceId:deviceId, + accessToken, refreshToken }, }); return { user, accessToken, refreshToken }; @@ -189,93 +191,96 @@ class AuthService { } public static async forgotPassword(input: ForgotPasswordDTO): Promise { - const { email, deviceId } = input; - - const user = await prisma.users.findUnique({ where: { email } }); - - if (!user) { - return { - success: true, - message: "If an account exists, a password reset link has been sent to your email", - } - } - - const resetToken = crypto.randomBytes(32).toString('hex'); - const expiresAt = new Date(Date.now() + 60 * 60 * 1000); - - // Use userTokens table (plural) - const existingToken = await prisma.userTokens.findFirst({ - where: { userId: user.id, deviceId } - }); - - if (existingToken) { - await prisma.userTokens.update({ - where: { id: existingToken.id }, - data: { - resetPasswordToken: resetToken, - resetPasswordExpires: expiresAt, - } - }); - } else { - await prisma.userTokens.create({ - data: { - userId: user.id, - deviceId, - resetPasswordToken: resetToken, - resetPasswordExpires: expiresAt, - refreshToken: crypto.randomBytes(40).toString('hex'), - accessToken: crypto.randomBytes(32).toString('hex') - } - }); - } - - console.log(`\n 🔐 RESET TOKEN FOR ${email}: ${resetToken}\n`); - + const { email, deviceId } = input; + + // Find user (don't reveal if exists for security) + const user = await prisma.users.findUnique({ where: { email } }); + + if (!user) { return { success: true, - message: "If an account exists, a password reset link has been sent to your email", + message: "If an account exists, an OTP has been sent to your email", } } + + // Generate 6-digit OTP + const otp = Math.floor(100000 + Math.random() * 900000).toString(); + const expiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes expiry + + // Delete any existing reset OTP for this user+device + await prisma.userVerifications.deleteMany({ + where: { + userId: user.id, + deviceId, + resetPasswordOTP: { not: null } + } + }); + + // Store OTP in userVerifications table + await prisma.userVerifications.create({ + data: { + userId: user.id, + deviceId:deviceId, + resetPasswordOTP: otp, + resetPasswordExpires: expiresAt, + token: crypto.randomBytes(32).toString('hex'), // required field + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // required field + } + }as any); + + // Log OTP for testing (remove in production) + console.log(`\n 🔐 PASSWORD RESET OTP FOR ${email}: ${otp}\n`); + + return { + success: true, + message: "If an account exists, an OTP has been sent to your email", + } +} + + public static async resetPassword(input: ResetPasswordDTO): Promise { - const { confirmationToken, newPassword, deviceId } = input; - - const tokenRecord = await prisma.userTokens.findFirst({ - where: { - resetPasswordToken: confirmationToken, - resetPasswordExpires: { gt: new Date() }, - }, - include: { user: true }, - }); - - if (!tokenRecord) { - throw new BadRequestError({ - msg: "Invalid or expired reset token", - errorCode: CustomErrorCode.AUTH_INVALID, - }); - } - - const saltRounds = 10; - const hashedPassword = await bcrypt.hash(newPassword, saltRounds); - - await prisma.userAuths.updateMany({ - where: { userId: tokenRecord.user.id }, - data: { passwordHash: hashedPassword }, - }); - - await prisma.userTokens.update({ - where: { id: tokenRecord.id }, - data: { - resetPasswordToken: null, - resetPasswordExpires: null, - }, + const { otp, newPassword, deviceId } = input; + + // Find valid OTP in userVerifications table + const otpRecord = await prisma.userVerifications.findFirst({ + where: { + resetPasswordOTP: otp, + resetPasswordExpires: { gt: new Date() }, + deviceId: deviceId, + }, + include: { user: true }, + }); + + if (!otpRecord || !otpRecord.user) { + throw new BadRequestError({ + msg: "Invalid or expired OTP", + errorCode: CustomErrorCode.AUTH_INVALID, }); - - return { - success: true, - message: "Password has been reset successfully", + } + + // Hash the new password using helper + const hashedPassword = await hashPassword(newPassword); + + // Update password in userAuths + await prisma.userAuths.updateMany({ + where: { userId: otpRecord.user.id }, + data: { passwordHash: hashedPassword }, + }); + + // Delete the used OTP (cleanup) + await prisma.userVerifications.deleteMany({ + where: { + userId: otpRecord.user.id, + resetPasswordOTP: otp, } + }); + + return { + success: true, + message: "Password has been reset successfully", } +} public static async refreshToken(input: RefreshTokenDTO): Promise { const {refreshToken, deviceId} = input; From 5ffabc0baf3c0f964da3901080250d4eabadcd8f Mon Sep 17 00:00:00 2001 From: Hadiqa Raza Date: Mon, 1 Jun 2026 15:23:56 +0300 Subject: [PATCH 7/9] fix: change UserAuths to one-to-one relation with Users --- .env | 2 +- prisma/schema.prisma | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.env b/.env index ea59c52..d14cd35 100644 --- a/.env +++ b/.env @@ -9,7 +9,7 @@ # server with the `prisma dev` CLI command, when not choosing any non-default ports or settings. The API key, unlike the # one found in a remote Prisma Postgres URL, does not contain any sensitive information. -DATABASE_URL="prisma+postgres://localhost:51213/?api_key=eyJkYXRhYmFzZVVybCI6InBvc3RncmVzOi8vcG9zdGdyZXM6cG9zdGdyZXNAbG9jYWxob3N0OjUxMjE0L3RlbXBsYXRlMT9zc2xtb2RlPWRpc2FibGUmY29ubmVjdGlvbl9saW1pdD0xMCZjb25uZWN0X3RpbWVvdXQ9MCZtYXhfaWRsZV9jb25uZWN0aW9uX2xpZmV0aW1lPTAmcG9vbF90aW1lb3V0PTAmc29ja2V0X3RpbWVvdXQ9MCIsIm5hbWUiOiJkZWZhdWx0Iiwic2hhZG93RGF0YWJhc2VVcmwiOiJwb3N0Z3JlczovL3Bvc3RncmVzOnBvc3RncmVzQGxvY2FsaG9zdDo1MTIxNS90ZW1wbGF0ZTE_c3NsbW9kZT1kaXNhYmxlJmNvbm5lY3Rpb25fbGltaXQ9MTAmY29ubmVjdF90aW1lb3V0PTAmbWF4X2lkbGVfY29ubmVjdGlvbl9saWZldGltZT0wJnBvb2xfdGltZW91dD0wJnNvY2tldF90aW1lb3V0PTAifQ" +DATABASE_URL="postgresql://postgres:Hafsahraza4%23@localhost:5432/travelbase_dev" ACCESS_TOKEN_SECRET=71b4c7d01c2cb2f2dd4ec05e0d991489c63194e1a9725e450342090468c262e5be5d9bd61813496a6942a9845fd039f1bafb736bbc97715ac210e807b119cdfb REFRESH_TOKEN_SECRET=71b4c7d01c2cb2f2dd4ec05e0d991489c63194e1a9725e450342090468c262e5be5d9bd61813496a6942a9845fd039f1bafb736bbc97715ac210e807b119cdfb JWT_SECRET=71b4c7d01c2cb2f2dd4ec05e0d991489c63194e1a9725e450342090468c262e5bd9bd61813496a6942a9845fd039f1bafb736bbc97715ac210e807b119cdfb \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 129ff76..76bd545 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -18,14 +18,14 @@ model Users { bio String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - UserAuths UserAuths[] + UserAuths UserAuths? UserVerifications UserVerifications[] UserTokens UserTokens[] } model UserAuths { id String @id @default(uuid()) - userId String + userId String @unique passwordHash String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -50,7 +50,7 @@ model UserTokens { userId String refreshToken String accessToken String - deviceId String? + deviceId String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user Users @relation(fields: [userId], references: [id]) From 522ec99b90873954e8ed2ad0e2aa4c1e59b4e066 Mon Sep 17 00:00:00 2001 From: Hadiqa Raza Date: Mon, 1 Jun 2026 15:31:21 +0300 Subject: [PATCH 8/9] fix: revert password helper to use bcryptjs with SALT_ROUNDS=12 --- src/helpers/password.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/helpers/password.ts b/src/helpers/password.ts index 9c482cc..0b33627 100644 --- a/src/helpers/password.ts +++ b/src/helpers/password.ts @@ -1,8 +1,9 @@ -import bcrypt from 'bcrypt'; +import bcrypt from 'bcryptjs'; + +const SALT_ROUNDS = 12; export async function hashPassword(password: string): Promise { - const saltRounds = 10; - return bcrypt.hash(password, saltRounds); + return bcrypt.hash(password, SALT_ROUNDS); } export async function verifyPassword(password: string, hash: string): Promise { From accf12da74c51e5040e48edf20a45db73fbe1041 Mon Sep 17 00:00:00 2001 From: Hadiqa Raza Date: Sun, 7 Jun 2026 22:55:06 +0300 Subject: [PATCH 9/9] feat: add admin token schema with single session enforcement - Add admins and AdminTokens models to schema - Create admin auth service with ACID transaction - Implement single session enforcement (revoke old tokens) - Add admin login, verify, logout endpoints - Register admin routes in server.ts --- .../20260607143946_init/migration.sql | 225 ++++++++++++++++++ prisma/schema.prisma | 36 ++- src/controllers/adminAuth.controller.ts | 36 +++ src/controllers/auth.controller.ts | 4 +- src/routes/admin.routes.ts | 17 ++ src/server.ts | 6 +- src/services/adminAuth.service.ts | 132 ++++++++++ 7 files changed, 446 insertions(+), 10 deletions(-) create mode 100644 prisma/migrations/20260607143946_init/migration.sql create mode 100644 src/controllers/adminAuth.controller.ts create mode 100644 src/routes/admin.routes.ts create mode 100644 src/services/adminAuth.service.ts diff --git a/prisma/migrations/20260607143946_init/migration.sql b/prisma/migrations/20260607143946_init/migration.sql new file mode 100644 index 0000000..2fc60b8 --- /dev/null +++ b/prisma/migrations/20260607143946_init/migration.sql @@ -0,0 +1,225 @@ +/* + Warnings: + + - A unique constraint covering the columns `[userId]` on the table `UserAuths` will be added. If there are existing duplicate values, this will fail. + - Made the column `deviceId` on table `UserTokens` required. This step will fail if there are existing NULL values in that column. + +*/ +-- CreateEnum +CREATE TYPE "FlightStatus" AS ENUM ('SCHEDULED', 'DELAYED', 'CANCELLED', 'COMPLETED'); + +-- CreateEnum +CREATE TYPE "BookingStatus" AS ENUM ('PENDING', 'CONFIRMED', 'CANCELLED', 'COMPLETED'); + +-- CreateEnum +CREATE TYPE "SeatClass" AS ENUM ('ECONOMY', 'BUSINESS', 'FIRST'); + +-- CreateEnum +CREATE TYPE "PaymentStatus" AS ENUM ('PENDING', 'SUCCESSFUL', 'FAILED', 'REFUNDED'); + +-- CreateEnum +CREATE TYPE "PassengerType" AS ENUM ('ADULT', 'CHILD', 'INFANT'); + +-- CreateEnum +CREATE TYPE "PaymentMethod" AS ENUM ('CARD', 'BANK_TRANSFER', 'WALLET'); + +-- AlterTable +ALTER TABLE "UserTokens" ALTER COLUMN "deviceId" SET NOT NULL; + +-- CreateTable +CREATE TABLE "Airlines" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "code" TEXT NOT NULL, + "logoUrl" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Airlines_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Airports" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "code" TEXT NOT NULL, + "city" TEXT NOT NULL, + "country" TEXT NOT NULL, + "timezone" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Airports_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Flights" ( + "id" TEXT NOT NULL, + "flightNumber" TEXT NOT NULL, + "airlineId" TEXT NOT NULL, + "originAirportId" TEXT NOT NULL, + "destinationAirportId" TEXT NOT NULL, + "departureTime" TIMESTAMP(3) NOT NULL, + "arrivalTime" TIMESTAMP(3) NOT NULL, + "status" "FlightStatus" NOT NULL DEFAULT 'SCHEDULED', + "totalSeats" INTEGER NOT NULL, + "availableSeats" INTEGER NOT NULL, + "price" DECIMAL(10,2) NOT NULL, + "currency" TEXT NOT NULL DEFAULT 'USD', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Flights_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Bookings" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "flightId" TEXT NOT NULL, + "status" "BookingStatus" NOT NULL DEFAULT 'PENDING', + "totalAmount" DECIMAL(10,2) NOT NULL, + "currency" TEXT NOT NULL DEFAULT 'USD', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Bookings_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Passengers" ( + "id" TEXT NOT NULL, + "bookingId" TEXT NOT NULL, + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "email" TEXT NOT NULL, + "phone" TEXT NOT NULL, + "passportNumber" TEXT NOT NULL, + "nationality" TEXT NOT NULL, + "dateOfBirth" TIMESTAMP(3) NOT NULL, + "type" "PassengerType" NOT NULL DEFAULT 'ADULT', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Passengers_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Seats" ( + "id" TEXT NOT NULL, + "flightId" TEXT NOT NULL, + "bookingId" TEXT, + "passengerId" TEXT, + "seatNumber" TEXT NOT NULL, + "class" "SeatClass" NOT NULL DEFAULT 'ECONOMY', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Seats_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Payments" ( + "id" TEXT NOT NULL, + "bookingId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "amount" DECIMAL(10,2) NOT NULL, + "currency" TEXT NOT NULL DEFAULT 'USD', + "status" "PaymentStatus" NOT NULL DEFAULT 'PENDING', + "paymentMethod" "PaymentMethod" NOT NULL, + "transactionRef" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Payments_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "admins" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password_hash" TEXT NOT NULL, + "name" TEXT, + "role" TEXT NOT NULL DEFAULT 'admin', + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "admins_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AdminTokens" ( + "id" TEXT NOT NULL, + "admin_id" TEXT NOT NULL, + "token_hash" TEXT NOT NULL, + "user_agent" TEXT, + "ip_address" TEXT, + "is_revoked" BOOLEAN NOT NULL DEFAULT false, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "expires_at" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "AdminTokens_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Airlines_code_key" ON "Airlines"("code"); + +-- CreateIndex +CREATE UNIQUE INDEX "Airports_code_key" ON "Airports"("code"); + +-- CreateIndex +CREATE UNIQUE INDEX "Flights_flightNumber_departureTime_key" ON "Flights"("flightNumber", "departureTime"); + +-- CreateIndex +CREATE UNIQUE INDEX "Seats_flightId_seatNumber_key" ON "Seats"("flightId", "seatNumber"); + +-- CreateIndex +CREATE UNIQUE INDEX "Payments_transactionRef_key" ON "Payments"("transactionRef"); + +-- CreateIndex +CREATE UNIQUE INDEX "admins_email_key" ON "admins"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "AdminTokens_token_hash_key" ON "AdminTokens"("token_hash"); + +-- CreateIndex +CREATE INDEX "AdminTokens_token_hash_is_revoked_idx" ON "AdminTokens"("token_hash", "is_revoked"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserAuths_userId_key" ON "UserAuths"("userId"); + +-- AddForeignKey +ALTER TABLE "Flights" ADD CONSTRAINT "Flights_airlineId_fkey" FOREIGN KEY ("airlineId") REFERENCES "Airlines"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Flights" ADD CONSTRAINT "Flights_originAirportId_fkey" FOREIGN KEY ("originAirportId") REFERENCES "Airports"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Flights" ADD CONSTRAINT "Flights_destinationAirportId_fkey" FOREIGN KEY ("destinationAirportId") REFERENCES "Airports"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Bookings" ADD CONSTRAINT "Bookings_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Bookings" ADD CONSTRAINT "Bookings_flightId_fkey" FOREIGN KEY ("flightId") REFERENCES "Flights"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Passengers" ADD CONSTRAINT "Passengers_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Bookings"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Seats" ADD CONSTRAINT "Seats_flightId_fkey" FOREIGN KEY ("flightId") REFERENCES "Flights"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Seats" ADD CONSTRAINT "Seats_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Bookings"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Seats" ADD CONSTRAINT "Seats_passengerId_fkey" FOREIGN KEY ("passengerId") REFERENCES "Passengers"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Payments" ADD CONSTRAINT "Payments_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Bookings"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Payments" ADD CONSTRAINT "Payments_userId_fkey" FOREIGN KEY ("userId") REFERENCES "Users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AdminTokens" ADD CONSTRAINT "AdminTokens_admin_id_fkey" FOREIGN KEY ("admin_id") REFERENCES "admins"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 19863ff..f57d649 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -18,9 +18,9 @@ model Users { bio String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - UserAuths UserAuths? + UserAuths UserAuths[] UserVerifications UserVerifications[] - UserTokens UserTokens? + UserTokens UserTokens[] Bookings Bookings[] Payments Payments[] } @@ -37,9 +37,9 @@ model UserAuths { model UserVerifications { id String @id @default(uuid()) userId String - token String // For device verification OTP - resetPasswordOTP String? // For password reset OTP - resetPasswordExpires DateTime? // Expiry for reset OTP + token String + resetPasswordOTP String? + resetPasswordExpires DateTime? deviceId String expiresAt DateTime createdAt DateTime @default(now()) @@ -168,7 +168,31 @@ model Payments { booking Bookings @relation(fields: [bookingId], references: [id]) user Users @relation(fields: [userId], references: [id]) } +model admins { + id String @id @default(uuid()) + email String @unique + password_hash String + name String? + role String @default("admin") + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + AdminTokens AdminTokens[] +} + +model AdminTokens { + id String @id @default(uuid()) + admin_id String + token_hash String @unique + user_agent String? + ip_address String? + is_revoked Boolean @default(false) + created_at DateTime @default(now()) + expires_at DateTime + + admin admins @relation(fields: [admin_id], references: [id]) + @@index([token_hash, is_revoked]) +} enum FlightStatus { SCHEDULED DELAYED @@ -206,4 +230,4 @@ enum PaymentMethod { CARD BANK_TRANSFER WALLET -} +} \ No newline at end of file diff --git a/src/controllers/adminAuth.controller.ts b/src/controllers/adminAuth.controller.ts new file mode 100644 index 0000000..dec3773 --- /dev/null +++ b/src/controllers/adminAuth.controller.ts @@ -0,0 +1,36 @@ +import { FastifyRequest, FastifyReply } from "fastify"; +import { AdminAuthService } from "../services/adminAuth.service"; + +export class AdminAuthController { + + public static async login(request: FastifyRequest, reply: FastifyReply) { + const { email, password } = request.body as any; + const user_agent = request.headers['user-agent']; + const ip_address = request.ip; + + const result = await AdminAuthService.login(email, password, user_agent, ip_address); + return reply.status(200).send(result); + } + + public static async logout(request: FastifyRequest, reply: FastifyReply) { + const token = (request.headers['authorization'] as string)?.replace('Bearer ', ''); + + if (!token) { + return reply.status(400).send({ success: false, message: "Token required" }); + } + + const result = await AdminAuthService.logout(token); + return reply.status(200).send(result); + } + + public static async verifyToken(request: FastifyRequest, reply: FastifyReply) { + const token = (request.headers['authorization'] as string)?.replace('Bearer ', ''); + + if (!token) { + return reply.status(401).send({ success: false, message: "Token required" }); + } + + await AdminAuthService.verifyToken(token); + return reply.status(200).send({ success: true, message: "Token is valid" }); + } +} \ No newline at end of file diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts index b2d9efd..b388c1a 100644 --- a/src/controllers/auth.controller.ts +++ b/src/controllers/auth.controller.ts @@ -53,11 +53,11 @@ class AuthController { } public static async resetPassword(request: FastifyRequest, reply: FastifyReply) { - const { token, newPassword } = ResetPasswordRequest.parse(request.body ?? {}); + const { newPassword } = ResetPasswordRequest.parse(request.body ?? {}); const result = await AuthService.resetPassword({ deviceId: request.headers['x-device-id'] as string, - token, newPassword, + otp: "" }); return sendResponse(reply, result); } diff --git a/src/routes/admin.routes.ts b/src/routes/admin.routes.ts new file mode 100644 index 0000000..92a7609 --- /dev/null +++ b/src/routes/admin.routes.ts @@ -0,0 +1,17 @@ +import { FastifyInstance } from "fastify"; +import { AdminAuthController } from "../controllers/adminAuth.controller.ts"; + +export async function adminRoutes(app: FastifyInstance) { + + app.post("/v1/admin/login", async (request, reply) => { + return AdminAuthController.login(request, reply); + }); + + app.post("/v1/admin/logout", async (request, reply) => { + return AdminAuthController.logout(request, reply); + }); + + app.get("/v1/admin/verify", async (request, reply) => { + return AdminAuthController.verifyToken(request, reply); + }); +} \ No newline at end of file diff --git a/src/server.ts b/src/server.ts index 62f9b4d..63ed7df 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,9 +1,10 @@ import Fastify from "fastify"; import cors from "@fastify/cors"; import rateLimit from "@fastify/rate-limit"; -import {healthRoutes, welcomeroutes, AuthRouter, UserRouter, FlightRouter} from './routes'; +//import {healthRoutes, welcomeroutes, AuthRouter, UserRouter, FlightRouter} from './routes'; import { welcomeroutes } from './routes'; import {healthRoutes, AuthRouter, UserRouter} from './routes'; +import { adminRoutes } from './routes/admin.routes'; import {config} from "./config"; import {ALLOWED_HEADERS, ALLOWED_METHODS} from "./enums"; import {fastifyErrorHandler} from "./exceptions"; @@ -27,7 +28,8 @@ export function buildServer() { app.addHook("preHandler", requireAuthHook); app.register(AuthRouter); app.register(UserRouter); - app.register(FlightRouter); + app.register(adminRoutes); + //app.register(FlightRouter); app.register(healthRoutes); app.register(welcomeroutes); diff --git a/src/services/adminAuth.service.ts b/src/services/adminAuth.service.ts new file mode 100644 index 0000000..75fbf79 --- /dev/null +++ b/src/services/adminAuth.service.ts @@ -0,0 +1,132 @@ +import { prisma } from "../lib/db"; +import { UnAuthorizedError, CustomErrorCode } from "../exceptions"; +import { generateJwtToken, TOKEN_TYPE, verifyPassword } from "../helpers"; +import crypto from "crypto"; +import jwt from 'jsonwebtoken'; + +// Helper function to hash tokens +function hashToken(token: string): string { + return crypto.createHash('sha256').update(token).digest('hex'); +} + +export class AdminAuthService { + + // Admin login with single session enforcement + public static async login(email: string, password: string, user_agent?: string, ip_address?: string) { + + // Find admin by email + const admin = await prisma.admins.findUnique({ + where: { email } + }); + + if (!admin) { + throw new UnAuthorizedError({ + msg: "Invalid email or password", + errorCode: CustomErrorCode.AUTH_INVALID + }); + } + + // Verify password + const passwordValid = await verifyPassword(password, admin.password_hash); + if (!passwordValid) { + throw new UnAuthorizedError({ + msg: "Invalid email or password", + errorCode: CustomErrorCode.AUTH_INVALID + }); + } + + // Use ACID transaction to ensure atomicity + const result = await prisma.$transaction(async (tx) => { + + // 1. Revoke all existing active tokens for this admin + await tx.adminTokens.updateMany({ + where: { + admin_id: admin.id, + is_revoked: false + }, + data: { + is_revoked: true + } + }); + + // 2. Generate new JWT token using simple approach + const jwtToken = jwt.sign( + { userId: admin.id, email: admin.email }, + process.env.JWT_SECRET || 'your_secret_key', + { expiresIn: '7d' } + ); + // 3. Hash the token for storage + const tokenHash = hashToken(jwtToken); + + // 4. Set expiry (7 days from now) + const expiresAt = new Date(); + expiresAt.setDate(expiresAt.getDate() + 7); + + // 5. Create new token record + await tx.adminTokens.create({ + data: { + admin_id: admin.id, + token_hash: tokenHash, + user_agent: user_agent || null, + ip_address: ip_address || null, + expires_at: expiresAt + } + }); + + return jwtToken; + }); + + // TODO: Clear old Redis cache key (uncomment when Redis is configured) + // await redis.del(`admin_session:${admin.id}`); + + return { + success: true, + message: "Login successful", + data: { token: result } + }; + } + + // Verify if token is valid and not revoked + public static async verifyToken(token: string) { + const tokenHash = hashToken(token); + + const tokenRecord = await prisma.adminTokens.findFirst({ + where: { + token_hash: tokenHash, + is_revoked: false, + expires_at: { + gt: new Date() + } + } + }); + + if (!tokenRecord) { + throw new UnAuthorizedError({ + msg: "Invalid or expired token. Please login again.", + errorCode: CustomErrorCode.AUTH_INVALID + }); + } + + return tokenRecord; + } + + // Logout - revoke the current token + public static async logout(token: string) { + const tokenHash = hashToken(token); + + await prisma.adminTokens.updateMany({ + where: { + token_hash: tokenHash, + is_revoked: false + }, + data: { + is_revoked: true + } + }); + + return { + success: true, + message: "Logged out successfully" + }; + } +} \ No newline at end of file