├── .env.example ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── app ├── 2fa │ ├── actions.ts │ ├── components.tsx │ ├── page.tsx │ ├── reset │ │ ├── actions.ts │ │ ├── components.tsx │ │ └── page.tsx │ └── setup │ │ ├── actions.ts │ │ ├── components.tsx │ │ └── page.tsx ├── actions.ts ├── components.tsx ├── forgot-password │ ├── actions.ts │ ├── components.tsx │ └── page.tsx ├── layout.tsx ├── login │ ├── actions.ts │ ├── components.tsx │ └── page.tsx ├── page.tsx ├── recovery-code │ └── page.tsx ├── reset-password │ ├── 2fa │ │ ├── actions.ts │ │ ├── components.tsx │ │ └── page.tsx │ ├── actions.ts │ ├── components.tsx │ ├── page.tsx │ └── verify-email │ │ ├── actions.ts │ │ ├── components.tsx │ │ └── page.tsx ├── settings │ ├── actions.ts │ ├── components.tsx │ └── page.tsx ├── signup │ ├── actions.ts │ ├── components.tsx │ └── page.tsx └── verify-email │ ├── actions.ts │ ├── components.tsx │ └── page.tsx ├── lib └── server │ ├── 2fa.ts │ ├── db.ts │ ├── email-verification.ts │ ├── email.ts │ ├── encryption.ts │ ├── password-reset.ts │ ├── password.ts │ ├── rate-limit.ts │ ├── request.ts │ ├── session.ts │ ├── user.ts │ └── utils.ts ├── middleware.ts ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── setup.sql └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | ENCRYPTION_KEY="" -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript", "prettier"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | .env 39 | sqlite.db -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "trailingComma": "none", 4 | "printWidth": 120 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 pilcrowOnPaper and contributors 2 | 3 | Permission to use, copy, modify, and/or distribute this software for 4 | any purpose with or without fee is hereby granted. 5 | 6 | THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL 7 | WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES 8 | OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE 9 | FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY 10 | DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN 11 | AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT 12 | OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Email and password example with 2FA in Next.js 2 | 3 | Built with SQLite. 4 | 5 | - Password check with HaveIBeenPwned 6 | - Email verification 7 | - 2FA with TOTP 8 | - 2FA recovery codes 9 | - Password reset 10 | - Login throttling and rate limiting 11 | 12 | Emails are just logged to the console. Rate limiting is implemented using JavaScript `Map`. 13 | 14 | ## Initialize project 15 | 16 | Create `sqlite.db` and run `setup.sql`. 17 | 18 | ``` 19 | sqlite3 sqlite.db 20 | ``` 21 | 22 | Create a .env file. Generate a 128 bit (16 byte) string, base64 encode it, and set it as `ENCRYPTION_KEY`. 23 | 24 | ```bash 25 | ENCRYPTION_KEY="L9pmqRJnO1ZJSQ2svbHuBA==" 26 | ``` 27 | 28 | > You can use OpenSSL to quickly generate a secure key. 29 | > 30 | > ```bash 31 | > openssl rand --base64 16 32 | > ``` 33 | 34 | Install dependencies and run the application: 35 | 36 | ``` 37 | pnpm i 38 | pnpm dev 39 | ``` 40 | 41 | ## Notes 42 | 43 | - We do not consider user enumeration to be a real vulnerability so please don't open issues on it. If you really need to prevent it, just don't use emails. 44 | - This example does not handle unexpected errors gracefully. 45 | - There are some major code duplications (specifically for 2FA) to keep the codebase simple. 46 | - TODO: You may need to rewrite some queries and use transactions to avoid race conditions when using MySQL, Postgres, etc. 47 | - TODO: This project relies on the `X-Forwarded-For` header for getting the client's IP address. 48 | - TODO: Logging should be implemented. 49 | -------------------------------------------------------------------------------- /app/2fa/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { totpBucket } from "@/lib/server/2fa"; 4 | import { globalPOSTRateLimit } from "@/lib/server/request"; 5 | import { getCurrentSession, setSessionAs2FAVerified } from "@/lib/server/session"; 6 | import { getUserTOTPKey } from "@/lib/server/user"; 7 | import { verifyTOTP } from "@oslojs/otp"; 8 | import { redirect } from "next/navigation"; 9 | 10 | export async function verify2FAAction(_prev: ActionResult, formData: FormData): Promise { 11 | if (!globalPOSTRateLimit()) { 12 | return { 13 | message: "Too many requests" 14 | }; 15 | } 16 | const { session, user } = getCurrentSession(); 17 | if (session === null) { 18 | return { 19 | message: "Not authenticated" 20 | }; 21 | } 22 | if (!user.emailVerified || !user.registered2FA || session.twoFactorVerified) { 23 | return { 24 | message: "Forbidden" 25 | }; 26 | } 27 | if (!totpBucket.check(user.id, 1)) { 28 | return { 29 | message: "Too many requests" 30 | }; 31 | } 32 | 33 | const code = formData.get("code"); 34 | if (typeof code !== "string") { 35 | return { 36 | message: "Invalid or missing fields" 37 | }; 38 | } 39 | if (code === "") { 40 | return { 41 | message: "Enter your code" 42 | }; 43 | } 44 | if (!totpBucket.consume(user.id, 1)) { 45 | return { 46 | message: "Too many requests" 47 | }; 48 | } 49 | const totpKey = getUserTOTPKey(user.id); 50 | if (totpKey === null) { 51 | return { 52 | message: "Forbidden" 53 | }; 54 | } 55 | if (!verifyTOTP(totpKey, 30, 6, code)) { 56 | return { 57 | message: "Invalid code" 58 | }; 59 | } 60 | totpBucket.reset(user.id); 61 | setSessionAs2FAVerified(session.id); 62 | return redirect("/"); 63 | } 64 | 65 | interface ActionResult { 66 | message: string; 67 | } 68 | -------------------------------------------------------------------------------- /app/2fa/components.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { verify2FAAction } from "./actions"; 4 | import { useFormState } from "react-dom"; 5 | 6 | const initial2FAVerificationState = { 7 | message: "" 8 | }; 9 | 10 | export function TwoFactorVerificationForm() { 11 | const [state, action] = useFormState(verify2FAAction, initial2FAVerificationState); 12 | return ( 13 |
14 | 15 | 16 |
17 | 18 |

{state.message}

19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/2fa/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { TwoFactorVerificationForm } from "./components"; 4 | import { getCurrentSession } from "@/lib/server/session"; 5 | import { redirect } from "next/navigation"; 6 | import { globalGETRateLimit } from "@/lib/server/request"; 7 | 8 | export default function Page() { 9 | if (!globalGETRateLimit()) { 10 | return "Too many requests"; 11 | } 12 | const { session, user } = getCurrentSession(); 13 | if (session === null) { 14 | return redirect("/login"); 15 | } 16 | if (!user.emailVerified) { 17 | return redirect("/verify-email"); 18 | } 19 | if (!user.registered2FA) { 20 | return redirect("/2fa/setup"); 21 | } 22 | if (session.twoFactorVerified) { 23 | return redirect("/"); 24 | } 25 | return ( 26 | <> 27 |

Two-factor authentication

28 |

Enter the code from your authenticator app.

29 | 30 | Use recovery code 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /app/2fa/reset/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { recoveryCodeBucket, resetUser2FAWithRecoveryCode } from "@/lib/server/2fa"; 4 | import { getCurrentSession } from "@/lib/server/session"; 5 | 6 | import { redirect } from "next/navigation"; 7 | 8 | export async function reset2FAAction(_prev: ActionResult, formData: FormData): Promise { 9 | const { session, user } = getCurrentSession(); 10 | if (session === null) { 11 | return { 12 | message: "Not authenticated" 13 | }; 14 | } 15 | if (!user.emailVerified || !user.registered2FA || session.twoFactorVerified) { 16 | return { 17 | message: "Forbidden" 18 | }; 19 | } 20 | if (!recoveryCodeBucket.check(user.id, 1)) { 21 | return { 22 | message: "Too many requests" 23 | }; 24 | } 25 | 26 | const code = formData.get("code"); 27 | if (typeof code !== "string") { 28 | return { 29 | message: "Invalid or missing fields" 30 | }; 31 | } 32 | if (code === "") { 33 | return { 34 | message: "Please enter your code" 35 | }; 36 | } 37 | if (!recoveryCodeBucket.consume(user.id, 1)) { 38 | return { 39 | message: "Too many requests" 40 | }; 41 | } 42 | const valid = resetUser2FAWithRecoveryCode(user.id, code); 43 | if (!valid) { 44 | return { 45 | message: "Invalid recovery code" 46 | }; 47 | } 48 | recoveryCodeBucket.reset(user.id); 49 | return redirect("/2fa/setup"); 50 | } 51 | 52 | interface ActionResult { 53 | message: string; 54 | } 55 | -------------------------------------------------------------------------------- /app/2fa/reset/components.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { reset2FAAction } from "./actions"; 4 | import { useFormState } from "react-dom"; 5 | 6 | const initial2FAResetState = { 7 | message: "" 8 | }; 9 | 10 | export function TwoFactorResetForm() { 11 | const [state, action] = useFormState(reset2FAAction, initial2FAResetState); 12 | return ( 13 |
14 | 15 | 16 |
17 | 18 |

{state.message ?? ""}

19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/2fa/reset/page.tsx: -------------------------------------------------------------------------------- 1 | import { TwoFactorResetForm } from "./components"; 2 | 3 | import { getCurrentSession } from "@/lib/server/session"; 4 | import { redirect } from "next/navigation"; 5 | import { globalGETRateLimit } from "@/lib/server/request"; 6 | 7 | export default function Page() { 8 | if (!globalGETRateLimit()) { 9 | return "Too many requests"; 10 | } 11 | const { session, user } = getCurrentSession(); 12 | if (session === null) { 13 | return redirect("/login"); 14 | } 15 | if (!user.emailVerified) { 16 | return redirect("/verify-email"); 17 | } 18 | if (!user.registered2FA) { 19 | return redirect("/2fa/setup"); 20 | } 21 | if (session.twoFactorVerified) { 22 | return redirect("/"); 23 | } 24 | return ( 25 | <> 26 |

Recover your account

27 | 28 | 29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /app/2fa/setup/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { RefillingTokenBucket } from "@/lib/server/rate-limit"; 4 | import { globalPOSTRateLimit } from "@/lib/server/request"; 5 | import { getCurrentSession, setSessionAs2FAVerified } from "@/lib/server/session"; 6 | import { updateUserTOTPKey } from "@/lib/server/user"; 7 | import { decodeBase64 } from "@oslojs/encoding"; 8 | import { verifyTOTP } from "@oslojs/otp"; 9 | import { redirect } from "next/navigation"; 10 | 11 | const totpUpdateBucket = new RefillingTokenBucket(3, 60 * 10); 12 | 13 | export async function setup2FAAction(_prev: ActionResult, formData: FormData): Promise { 14 | if (!globalPOSTRateLimit()) { 15 | return { 16 | message: "Too many requests" 17 | }; 18 | } 19 | const { session, user } = getCurrentSession(); 20 | if (session === null) { 21 | return { 22 | message: "Not authenticated" 23 | }; 24 | } 25 | if (!user.emailVerified) { 26 | return { 27 | message: "Forbidden" 28 | }; 29 | } 30 | if (user.registered2FA && !session.twoFactorVerified) { 31 | return { 32 | message: "Forbidden" 33 | }; 34 | } 35 | if (!totpUpdateBucket.check(user.id, 1)) { 36 | return { 37 | message: "Too many requests" 38 | }; 39 | } 40 | 41 | const encodedKey = formData.get("key"); 42 | const code = formData.get("code"); 43 | if (typeof encodedKey !== "string" || typeof code !== "string") { 44 | return { 45 | message: "Invalid or missing fields" 46 | }; 47 | } 48 | if (code === "") { 49 | return { 50 | message: "Please enter your code" 51 | }; 52 | } 53 | if (encodedKey.length !== 28) { 54 | return { 55 | message: "Please enter your code" 56 | }; 57 | } 58 | let key: Uint8Array; 59 | try { 60 | key = decodeBase64(encodedKey); 61 | } catch { 62 | return { 63 | message: "Invalid key" 64 | }; 65 | } 66 | if (key.byteLength !== 20) { 67 | return { 68 | message: "Invalid key" 69 | }; 70 | } 71 | if (!totpUpdateBucket.consume(user.id, 1)) { 72 | return { 73 | message: "Too many requests" 74 | }; 75 | } 76 | if (!verifyTOTP(key, 30, 6, code)) { 77 | return { 78 | message: "Invalid code" 79 | }; 80 | } 81 | updateUserTOTPKey(session.userId, key); 82 | setSessionAs2FAVerified(session.id); 83 | return redirect("/recovery-code"); 84 | } 85 | 86 | interface ActionResult { 87 | message: string; 88 | } 89 | -------------------------------------------------------------------------------- /app/2fa/setup/components.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { setup2FAAction } from "./actions"; 4 | import { useFormState } from "react-dom"; 5 | 6 | const initial2FASetUpState = { 7 | message: "" 8 | }; 9 | 10 | export function TwoFactorSetUpForm(props: { encodedTOTPKey: string }) { 11 | const [state, action] = useFormState(setup2FAAction, initial2FASetUpState); 12 | return ( 13 |
14 | 15 | 16 | 17 |
18 | 19 |

{state.message}

20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app/2fa/setup/page.tsx: -------------------------------------------------------------------------------- 1 | import { TwoFactorSetUpForm } from "./components"; 2 | 3 | import { getCurrentSession } from "@/lib/server/session"; 4 | import { encodeBase64 } from "@oslojs/encoding"; 5 | import { createTOTPKeyURI } from "@oslojs/otp"; 6 | import { redirect } from "next/navigation"; 7 | import { renderSVG } from "uqr"; 8 | import { globalGETRateLimit } from "@/lib/server/request"; 9 | 10 | export default function Page() { 11 | if (!globalGETRateLimit()) { 12 | return "Too many requests"; 13 | } 14 | const { session, user } = getCurrentSession(); 15 | if (session === null) { 16 | return redirect("/login"); 17 | } 18 | if (!user.emailVerified) { 19 | return redirect("/verify-email"); 20 | } 21 | if (user.registered2FA && !session.twoFactorVerified) { 22 | return redirect("/2fa"); 23 | } 24 | 25 | const totpKey = new Uint8Array(20); 26 | crypto.getRandomValues(totpKey); 27 | const encodedTOTPKey = encodeBase64(totpKey); 28 | const keyURI = createTOTPKeyURI("Demo", user.username, totpKey, 30, 6); 29 | const qrcode = renderSVG(keyURI); 30 | return ( 31 | <> 32 |

Set up two-factor authentication

33 |
42 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /app/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { globalPOSTRateLimit } from "@/lib/server/request"; 4 | import { deleteSessionTokenCookie, getCurrentSession, invalidateSession } from "@/lib/server/session"; 5 | import { redirect } from "next/navigation"; 6 | 7 | export async function logoutAction(): Promise { 8 | if (!globalPOSTRateLimit()) { 9 | return { 10 | message: "Too many requests" 11 | }; 12 | } 13 | const { session } = getCurrentSession(); 14 | if (session === null) { 15 | return { 16 | message: "Not authenticated" 17 | }; 18 | } 19 | invalidateSession(session.id); 20 | deleteSessionTokenCookie(); 21 | return redirect("/login"); 22 | } 23 | 24 | interface ActionResult { 25 | message: string; 26 | } 27 | -------------------------------------------------------------------------------- /app/components.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { logoutAction } from "./actions"; 4 | import { useFormState } from "react-dom"; 5 | 6 | const initialState = { 7 | message: "" 8 | }; 9 | 10 | export function LogoutButton() { 11 | const [, action] = useFormState(logoutAction, initialState); 12 | return ( 13 |
14 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/forgot-password/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { verifyEmailInput } from "@/lib/server/email"; 4 | import { 5 | createPasswordResetSession, 6 | invalidateUserPasswordResetSessions, 7 | sendPasswordResetEmail, 8 | setPasswordResetSessionTokenCookie 9 | } from "@/lib/server/password-reset"; 10 | import { RefillingTokenBucket } from "@/lib/server/rate-limit"; 11 | import { globalPOSTRateLimit } from "@/lib/server/request"; 12 | import { generateSessionToken } from "@/lib/server/session"; 13 | import { getUserFromEmail } from "@/lib/server/user"; 14 | import { headers } from "next/headers"; 15 | import { redirect } from "next/navigation"; 16 | 17 | const passwordResetEmailIPBucket = new RefillingTokenBucket(3, 60); 18 | const passwordResetEmailUserBucket = new RefillingTokenBucket(3, 60); 19 | 20 | export async function forgotPasswordAction(_prev: ActionResult, formData: FormData): Promise { 21 | if (!globalPOSTRateLimit()) { 22 | return { 23 | message: "Too many requests" 24 | }; 25 | } 26 | // TODO: Assumes X-Forwarded-For is always included. 27 | const clientIP = headers().get("X-Forwarded-For"); 28 | if (clientIP !== null && !passwordResetEmailIPBucket.check(clientIP, 1)) { 29 | return { 30 | message: "Too many requests" 31 | }; 32 | } 33 | 34 | const email = formData.get("email"); 35 | if (typeof email !== "string") { 36 | return { 37 | message: "Invalid or missing fields" 38 | }; 39 | } 40 | if (!verifyEmailInput(email)) { 41 | return { 42 | message: "Invalid email" 43 | }; 44 | } 45 | const user = getUserFromEmail(email); 46 | if (user === null) { 47 | return { 48 | message: "Account does not exist" 49 | }; 50 | } 51 | if (clientIP !== null && !passwordResetEmailIPBucket.consume(clientIP, 1)) { 52 | return { 53 | message: "Too many requests" 54 | }; 55 | } 56 | if (!passwordResetEmailUserBucket.consume(user.id, 1)) { 57 | return { 58 | message: "Too many requests" 59 | }; 60 | } 61 | invalidateUserPasswordResetSessions(user.id); 62 | const sessionToken = generateSessionToken(); 63 | const session = createPasswordResetSession(sessionToken, user.id, user.email); 64 | 65 | sendPasswordResetEmail(session.email, session.code); 66 | setPasswordResetSessionTokenCookie(sessionToken, session.expiresAt); 67 | return redirect("/reset-password/verify-email"); 68 | } 69 | 70 | interface ActionResult { 71 | message: string; 72 | } 73 | -------------------------------------------------------------------------------- /app/forgot-password/components.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useFormState } from "react-dom"; 4 | import { forgotPasswordAction } from "./actions"; 5 | 6 | const initialForgotPasswordState = { 7 | message: "" 8 | }; 9 | 10 | export function ForgotPasswordForm() { 11 | const [state, action] = useFormState(forgotPasswordAction, initialForgotPasswordState); 12 | return ( 13 |
14 | 15 | 16 |
17 | 18 |

{state.message}

19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/forgot-password/page.tsx: -------------------------------------------------------------------------------- 1 | import { ForgotPasswordForm } from "./components"; 2 | import Link from "next/link"; 3 | 4 | import { globalGETRateLimit } from "@/lib/server/request"; 5 | 6 | export default function Page() { 7 | if (!globalGETRateLimit()) { 8 | return "Too many requests"; 9 | } 10 | return ( 11 | <> 12 |

Forgot your password?

13 | 14 | Sign in 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | 3 | export const metadata: Metadata = { 4 | title: "Email and password example with 2FA in Next.js" 5 | }; 6 | 7 | export default function RootLayout({ 8 | children 9 | }: Readonly<{ 10 | children: React.ReactNode; 11 | }>) { 12 | return ( 13 | 14 | {children} 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/login/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { verifyEmailInput } from "@/lib/server/email"; 4 | import { verifyPasswordHash } from "@/lib/server/password"; 5 | import { RefillingTokenBucket, Throttler } from "@/lib/server/rate-limit"; 6 | import { createSession, generateSessionToken, setSessionTokenCookie } from "@/lib/server/session"; 7 | import { getUserFromEmail, getUserPasswordHash } from "@/lib/server/user"; 8 | import { headers } from "next/headers"; 9 | import { redirect } from "next/navigation"; 10 | import { globalPOSTRateLimit } from "@/lib/server/request"; 11 | 12 | import type { SessionFlags } from "@/lib/server/session"; 13 | 14 | const throttler = new Throttler([1, 2, 4, 8, 16, 30, 60, 180, 300]); 15 | const ipBucket = new RefillingTokenBucket(20, 1); 16 | 17 | export async function loginAction(_prev: ActionResult, formData: FormData): Promise { 18 | if (!globalPOSTRateLimit()) { 19 | return { 20 | message: "Too many requests" 21 | }; 22 | } 23 | // TODO: Assumes X-Forwarded-For is always included. 24 | const clientIP = headers().get("X-Forwarded-For"); 25 | if (clientIP !== null && !ipBucket.check(clientIP, 1)) { 26 | return { 27 | message: "Too many requests" 28 | }; 29 | } 30 | 31 | const email = formData.get("email"); 32 | const password = formData.get("password"); 33 | if (typeof email !== "string" || typeof password !== "string") { 34 | return { 35 | message: "Invalid or missing fields" 36 | }; 37 | } 38 | if (email === "" || password === "") { 39 | return { 40 | message: "Please enter your email and password." 41 | }; 42 | } 43 | if (!verifyEmailInput(email)) { 44 | return { 45 | message: "Invalid email" 46 | }; 47 | } 48 | const user = getUserFromEmail(email); 49 | if (user === null) { 50 | return { 51 | message: "Account does not exist" 52 | }; 53 | } 54 | if (clientIP !== null && !ipBucket.consume(clientIP, 1)) { 55 | return { 56 | message: "Too many requests" 57 | }; 58 | } 59 | if (!throttler.consume(user.id)) { 60 | return { 61 | message: "Too many requests" 62 | }; 63 | } 64 | const passwordHash = getUserPasswordHash(user.id); 65 | const validPassword = await verifyPasswordHash(passwordHash, password); 66 | if (!validPassword) { 67 | return { 68 | message: "Invalid password" 69 | }; 70 | } 71 | throttler.reset(user.id); 72 | const sessionFlags: SessionFlags = { 73 | twoFactorVerified: false 74 | }; 75 | const sessionToken = generateSessionToken(); 76 | const session = createSession(sessionToken, user.id, sessionFlags); 77 | setSessionTokenCookie(sessionToken, session.expiresAt); 78 | 79 | if (!user.emailVerified) { 80 | return redirect("/verify-email"); 81 | } 82 | if (!user.registered2FA) { 83 | return redirect("/2fa/setup"); 84 | } 85 | return redirect("/2fa"); 86 | } 87 | 88 | interface ActionResult { 89 | message: string; 90 | } 91 | -------------------------------------------------------------------------------- /app/login/components.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { loginAction } from "./actions"; 4 | import { useFormState } from "react-dom"; 5 | 6 | const initialState = { 7 | message: "" 8 | }; 9 | 10 | export function LoginForm() { 11 | const [state, action] = useFormState(loginAction, initialState); 12 | 13 | return ( 14 |
15 | 16 | 17 |
18 | 19 | 20 |
21 | 22 |

{state.message}

23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /app/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { LoginForm } from "./components"; 2 | import Link from "next/link"; 3 | 4 | import { getCurrentSession } from "@/lib/server/session"; 5 | import { redirect } from "next/navigation"; 6 | import { globalGETRateLimit } from "@/lib/server/request"; 7 | 8 | export default function Page() { 9 | if (!globalGETRateLimit()) { 10 | return "Too many requests"; 11 | } 12 | const { session, user } = getCurrentSession(); 13 | if (session !== null) { 14 | if (!user.emailVerified) { 15 | return redirect("/verify-email"); 16 | } 17 | if (!user.registered2FA) { 18 | return redirect("/2fa/setup"); 19 | } 20 | if (!session.twoFactorVerified) { 21 | return redirect("/2fa"); 22 | } 23 | return redirect("/"); 24 | } 25 | return ( 26 | <> 27 |

Sign in

28 | 29 | Create an account 30 | Forgot password? 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { LogoutButton } from "./components"; 2 | import Link from "next/link"; 3 | 4 | import { getCurrentSession } from "@/lib/server/session"; 5 | import { redirect } from "next/navigation"; 6 | import { globalGETRateLimit } from "@/lib/server/request"; 7 | 8 | export default function Page() { 9 | if (!globalGETRateLimit()) { 10 | return "Too many requests"; 11 | } 12 | const { session, user } = getCurrentSession(); 13 | if (session === null) { 14 | return redirect("/login"); 15 | } 16 | if (!user.emailVerified) { 17 | return redirect("/verify-email"); 18 | } 19 | if (!user.registered2FA) { 20 | return redirect("/2fa/setup"); 21 | } 22 | if (!session.twoFactorVerified) { 23 | return redirect("/2fa"); 24 | } 25 | return ( 26 | <> 27 |
28 | Home 29 | Settings 30 |
31 |
32 |

Hi {user.username}!

33 | 34 |
35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /app/recovery-code/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { getCurrentSession } from "@/lib/server/session"; 4 | import { getUserRecoverCode } from "@/lib/server/user"; 5 | import { redirect } from "next/navigation"; 6 | import { globalGETRateLimit } from "@/lib/server/request"; 7 | 8 | export default function Page() { 9 | if (!globalGETRateLimit()) { 10 | return "Too many requests"; 11 | } 12 | const { session, user } = getCurrentSession(); 13 | if (session === null) { 14 | return redirect("/login"); 15 | } 16 | if (!user.emailVerified) { 17 | return redirect("/verify-email"); 18 | } 19 | if (!user.registered2FA) { 20 | return redirect("/2fa/setup"); 21 | } 22 | if (!session.twoFactorVerified) { 23 | return redirect("/2fa"); 24 | } 25 | const recoveryCode = getUserRecoverCode(user.id); 26 | return ( 27 | <> 28 |

Recovery code

29 |

Your recovery code is: {recoveryCode}

30 |

You can use this recovery code if you lose access to your second factors.

31 | Next 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /app/reset-password/2fa/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { recoveryCodeBucket, resetUser2FAWithRecoveryCode, totpBucket } from "@/lib/server/2fa"; 4 | import { setPasswordResetSessionAs2FAVerified, validatePasswordResetSessionRequest } from "@/lib/server/password-reset"; 5 | import { globalPOSTRateLimit } from "@/lib/server/request"; 6 | import { getUserTOTPKey } from "@/lib/server/user"; 7 | import { verifyTOTP } from "@oslojs/otp"; 8 | import { redirect } from "next/navigation"; 9 | 10 | export async function verifyPasswordReset2FAWithTOTPAction( 11 | _prev: ActionResult, 12 | formData: FormData 13 | ): Promise { 14 | if (!globalPOSTRateLimit()) { 15 | return { 16 | message: "Too many requests" 17 | }; 18 | } 19 | const { session, user } = validatePasswordResetSessionRequest(); 20 | if (session === null) { 21 | return { 22 | message: "Not authenticated" 23 | }; 24 | } 25 | if (!session.emailVerified || !user.registered2FA || session.twoFactorVerified) { 26 | return { 27 | message: "Forbidden" 28 | }; 29 | } 30 | if (!totpBucket.check(session.userId, 1)) { 31 | return { 32 | message: "Too many requests" 33 | }; 34 | } 35 | 36 | const code = formData.get("code"); 37 | if (typeof code !== "string") { 38 | return { 39 | message: "Invalid or missing fields" 40 | }; 41 | } 42 | if (code === "") { 43 | return { 44 | message: "Please enter your code" 45 | }; 46 | } 47 | const totpKey = getUserTOTPKey(session.userId); 48 | if (totpKey === null) { 49 | return { 50 | message: "Forbidden" 51 | }; 52 | } 53 | if (!totpBucket.consume(session.userId, 1)) { 54 | return { 55 | message: "Too many requests" 56 | }; 57 | } 58 | if (!verifyTOTP(totpKey, 30, 6, code)) { 59 | return { 60 | message: "Invalid code" 61 | }; 62 | } 63 | totpBucket.reset(session.userId); 64 | setPasswordResetSessionAs2FAVerified(session.id); 65 | return redirect("/reset-password"); 66 | } 67 | 68 | export async function verifyPasswordReset2FAWithRecoveryCodeAction( 69 | _prev: ActionResult, 70 | formData: FormData 71 | ): Promise { 72 | if (!globalPOSTRateLimit()) { 73 | return { 74 | message: "Too many requests" 75 | }; 76 | } 77 | const { session, user } = validatePasswordResetSessionRequest(); 78 | if (session === null) { 79 | return { 80 | message: "Not authenticated" 81 | }; 82 | } 83 | if (!session.emailVerified || !user.registered2FA || session.twoFactorVerified) { 84 | return { 85 | message: "Forbidden" 86 | }; 87 | } 88 | 89 | if (!recoveryCodeBucket.check(session.userId, 1)) { 90 | return { 91 | message: "Too many requests" 92 | }; 93 | } 94 | const code = formData.get("code"); 95 | if (typeof code !== "string") { 96 | return { 97 | message: "Invalid or missing fields" 98 | }; 99 | } 100 | if (code === "") { 101 | return { 102 | message: "Please enter your code" 103 | }; 104 | } 105 | if (!recoveryCodeBucket.consume(session.userId, 1)) { 106 | return { 107 | message: "Too many requests" 108 | }; 109 | } 110 | const valid = resetUser2FAWithRecoveryCode(session.userId, code); 111 | if (!valid) { 112 | return { 113 | message: "Invalid code" 114 | }; 115 | } 116 | recoveryCodeBucket.reset(session.userId); 117 | return redirect("/reset-password"); 118 | } 119 | 120 | interface ActionResult { 121 | message: string; 122 | } 123 | -------------------------------------------------------------------------------- /app/reset-password/2fa/components.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useFormState } from "react-dom"; 4 | import { verifyPasswordReset2FAWithRecoveryCodeAction, verifyPasswordReset2FAWithTOTPAction } from "./actions"; 5 | 6 | const initialPasswordResetTOTPState = { 7 | message: "" 8 | }; 9 | 10 | export function PasswordResetTOTPForm() { 11 | const [state, action] = useFormState(verifyPasswordReset2FAWithTOTPAction, initialPasswordResetTOTPState); 12 | return ( 13 |
14 | 15 | 16 |
17 | 18 |

{state.message}

19 |
20 | ); 21 | } 22 | 23 | const initialPasswordResetRecoveryCodeState = { 24 | message: "" 25 | }; 26 | 27 | export function PasswordResetRecoveryCodeForm() { 28 | const [state, action] = useFormState( 29 | verifyPasswordReset2FAWithRecoveryCodeAction, 30 | initialPasswordResetRecoveryCodeState 31 | ); 32 | return ( 33 |
34 | 35 | 36 |
37 |
38 | 39 |

{state.message}

40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /app/reset-password/2fa/page.tsx: -------------------------------------------------------------------------------- 1 | import { PasswordResetRecoveryCodeForm, PasswordResetTOTPForm } from "./components"; 2 | 3 | import { validatePasswordResetSessionRequest } from "@/lib/server/password-reset"; 4 | import { globalGETRateLimit } from "@/lib/server/request"; 5 | import { redirect } from "next/navigation"; 6 | 7 | export default function Page() { 8 | if (!globalGETRateLimit()) { 9 | return "Too many requests"; 10 | } 11 | const { session, user } = validatePasswordResetSessionRequest(); 12 | 13 | if (session === null) { 14 | return redirect("/forgot-password"); 15 | } 16 | if (!session.emailVerified) { 17 | return redirect("/reset-password/verify-email"); 18 | } 19 | if (!user.registered2FA) { 20 | return redirect("/reset-password"); 21 | } 22 | if (session.twoFactorVerified) { 23 | return redirect("/reset-password"); 24 | } 25 | return ( 26 | <> 27 |

Two-factor authentication

28 |

Enter the code from your authenticator app.

29 | 30 |
31 |

Use your recovery code instead

32 | 33 |
34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /app/reset-password/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { verifyPasswordStrength } from "@/lib/server/password"; 4 | import { 5 | deletePasswordResetSessionTokenCookie, 6 | invalidateUserPasswordResetSessions, 7 | validatePasswordResetSessionRequest 8 | } from "@/lib/server/password-reset"; 9 | import { 10 | createSession, 11 | generateSessionToken, 12 | invalidateUserSessions, 13 | setSessionTokenCookie 14 | } from "@/lib/server/session"; 15 | import { updateUserPassword } from "@/lib/server/user"; 16 | import { redirect } from "next/navigation"; 17 | import { globalPOSTRateLimit } from "@/lib/server/request"; 18 | 19 | import type { SessionFlags } from "@/lib/server/session"; 20 | 21 | export async function resetPasswordAction(_prev: ActionResult, formData: FormData): Promise { 22 | if (!globalPOSTRateLimit()) { 23 | return { 24 | message: "Too many requests" 25 | }; 26 | } 27 | const { session: passwordResetSession, user } = validatePasswordResetSessionRequest(); 28 | if (passwordResetSession === null) { 29 | return { 30 | message: "Not authenticated" 31 | }; 32 | } 33 | if (!passwordResetSession.emailVerified) { 34 | return { 35 | message: "Forbidden" 36 | }; 37 | } 38 | if (user.registered2FA && !passwordResetSession.twoFactorVerified) { 39 | return { 40 | message: "Forbidden" 41 | }; 42 | } 43 | 44 | const password = formData.get("password"); 45 | if (typeof password !== "string") { 46 | return { 47 | message: "Invalid or missing fields" 48 | }; 49 | } 50 | 51 | const strongPassword = await verifyPasswordStrength(password); 52 | if (!strongPassword) { 53 | return { 54 | message: "Weak password" 55 | }; 56 | } 57 | invalidateUserPasswordResetSessions(passwordResetSession.userId); 58 | invalidateUserSessions(passwordResetSession.userId); 59 | await updateUserPassword(passwordResetSession.userId, password); 60 | 61 | const sessionFlags: SessionFlags = { 62 | twoFactorVerified: passwordResetSession.twoFactorVerified 63 | }; 64 | const sessionToken = generateSessionToken(); 65 | const session = createSession(sessionToken, user.id, sessionFlags); 66 | setSessionTokenCookie(sessionToken, session.expiresAt); 67 | deletePasswordResetSessionTokenCookie(); 68 | return redirect("/"); 69 | } 70 | 71 | interface ActionResult { 72 | message: string; 73 | } 74 | -------------------------------------------------------------------------------- /app/reset-password/components.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useFormState } from "react-dom"; 4 | import { resetPasswordAction } from "./actions"; 5 | 6 | const initialPasswordResetState = { 7 | message: "" 8 | }; 9 | 10 | export function PasswordResetForm() { 11 | const [state, action] = useFormState(resetPasswordAction, initialPasswordResetState); 12 | return ( 13 |
14 | 15 | 16 |
17 | 18 |

{state.message}

19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/reset-password/page.tsx: -------------------------------------------------------------------------------- 1 | import { PasswordResetForm } from "./components"; 2 | 3 | import { validatePasswordResetSessionRequest } from "@/lib/server/password-reset"; 4 | import { globalGETRateLimit } from "@/lib/server/request"; 5 | import { redirect } from "next/navigation"; 6 | 7 | export default function Page() { 8 | if (!globalGETRateLimit()) { 9 | return "Too many requests"; 10 | } 11 | const { session, user } = validatePasswordResetSessionRequest(); 12 | if (session === null) { 13 | return redirect("/forgot-password"); 14 | } 15 | if (!session.emailVerified) { 16 | return redirect("/reset-password/verify-email"); 17 | } 18 | if (user.registered2FA && !session.twoFactorVerified) { 19 | return redirect("/reset-password/2fa"); 20 | } 21 | return ( 22 | <> 23 |

Enter your new password

24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /app/reset-password/verify-email/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { 4 | setPasswordResetSessionAsEmailVerified, 5 | validatePasswordResetSessionRequest 6 | } from "@/lib/server/password-reset"; 7 | import { ExpiringTokenBucket } from "@/lib/server/rate-limit"; 8 | import { globalPOSTRateLimit } from "@/lib/server/request"; 9 | import { setUserAsEmailVerifiedIfEmailMatches } from "@/lib/server/user"; 10 | import { redirect } from "next/navigation"; 11 | 12 | const emailVerificationBucket = new ExpiringTokenBucket(5, 60 * 30); 13 | 14 | export async function verifyPasswordResetEmailAction(_prev: ActionResult, formData: FormData): Promise { 15 | if (!globalPOSTRateLimit()) { 16 | return { 17 | message: "Too many requests" 18 | }; 19 | } 20 | const { session } = validatePasswordResetSessionRequest(); 21 | if (session === null) { 22 | return { 23 | message: "Not authenticated" 24 | }; 25 | } 26 | if (session.emailVerified) { 27 | return { 28 | message: "Forbidden" 29 | }; 30 | } 31 | if (!emailVerificationBucket.check(session.userId, 1)) { 32 | return { 33 | message: "Too many requests" 34 | }; 35 | } 36 | 37 | const code = formData.get("code"); 38 | if (typeof code !== "string") { 39 | return { 40 | message: "Invalid or missing fields" 41 | }; 42 | } 43 | if (code === "") { 44 | return { 45 | message: "Please enter your code" 46 | }; 47 | } 48 | if (!emailVerificationBucket.consume(session.userId, 1)) { 49 | return { message: "Too many requests" }; 50 | } 51 | if (code !== session.code) { 52 | return { 53 | message: "Incorrect code" 54 | }; 55 | } 56 | emailVerificationBucket.reset(session.userId); 57 | setPasswordResetSessionAsEmailVerified(session.id); 58 | const emailMatches = setUserAsEmailVerifiedIfEmailMatches(session.userId, session.email); 59 | if (!emailMatches) { 60 | return { 61 | message: "Please restart the process" 62 | }; 63 | } 64 | return redirect("/reset-password/2fa"); 65 | } 66 | 67 | interface ActionResult { 68 | message: string; 69 | } 70 | -------------------------------------------------------------------------------- /app/reset-password/verify-email/components.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useFormState } from "react-dom"; 4 | import { verifyPasswordResetEmailAction } from "./actions"; 5 | 6 | const initialPasswordResetEmailVerificationState = { 7 | message: "" 8 | }; 9 | 10 | export function PasswordResetEmailVerificationForm() { 11 | const [state, action] = useFormState(verifyPasswordResetEmailAction, initialPasswordResetEmailVerificationState); 12 | return ( 13 |
14 | 15 | 16 | 17 |

{state.message}

18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /app/reset-password/verify-email/page.tsx: -------------------------------------------------------------------------------- 1 | import { PasswordResetEmailVerificationForm } from "./components"; 2 | 3 | import { validatePasswordResetSessionRequest } from "@/lib/server/password-reset"; 4 | import { globalGETRateLimit } from "@/lib/server/request"; 5 | import { redirect } from "next/navigation"; 6 | 7 | export default function Page() { 8 | if (!globalGETRateLimit()) { 9 | return "Too many requests"; 10 | } 11 | const { session } = validatePasswordResetSessionRequest(); 12 | if (session === null) { 13 | return redirect("/forgot-password"); 14 | } 15 | if (session.emailVerified) { 16 | if (!session.twoFactorVerified) { 17 | return redirect("/reset-password/2fa"); 18 | } 19 | return redirect("/reset-password"); 20 | } 21 | return ( 22 | <> 23 |

Verify your email address

24 |

We sent an 8-digit code to {session.email}.

25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /app/settings/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { verifyPasswordHash, verifyPasswordStrength } from "@/lib/server/password"; 4 | import { ExpiringTokenBucket } from "@/lib/server/rate-limit"; 5 | import { 6 | createSession, 7 | generateSessionToken, 8 | getCurrentSession, 9 | invalidateUserSessions, 10 | setSessionTokenCookie 11 | } from "@/lib/server/session"; 12 | import { getUserPasswordHash, resetUserRecoveryCode, updateUserPassword } from "@/lib/server/user"; 13 | import { 14 | createEmailVerificationRequest, 15 | sendVerificationEmail, 16 | sendVerificationEmailBucket, 17 | setEmailVerificationRequestCookie 18 | } from "@/lib/server/email-verification"; 19 | import { checkEmailAvailability, verifyEmailInput } from "@/lib/server/email"; 20 | import { redirect } from "next/navigation"; 21 | import { globalPOSTRateLimit } from "@/lib/server/request"; 22 | 23 | import type { SessionFlags } from "@/lib/server/session"; 24 | 25 | const passwordUpdateBucket = new ExpiringTokenBucket(5, 60 * 30); 26 | 27 | export async function updatePasswordAction(_prev: ActionResult, formData: FormData): Promise { 28 | if (!globalPOSTRateLimit()) { 29 | return { 30 | message: "Too many requests" 31 | }; 32 | } 33 | const { session, user } = getCurrentSession(); 34 | if (session === null) { 35 | return { 36 | message: "Not authenticated" 37 | }; 38 | } 39 | if (user.registered2FA && !session.twoFactorVerified) { 40 | return { 41 | message: "Forbidden" 42 | }; 43 | } 44 | if (!passwordUpdateBucket.check(session.id, 1)) { 45 | return { 46 | message: "Too many requests" 47 | }; 48 | } 49 | 50 | const password = formData.get("password"); 51 | const newPassword = formData.get("new_password"); 52 | if (typeof password !== "string" || typeof newPassword !== "string") { 53 | return { 54 | message: "Invalid or missing fields" 55 | }; 56 | } 57 | const strongPassword = await verifyPasswordStrength(newPassword); 58 | if (!strongPassword) { 59 | return { 60 | message: "Weak password" 61 | }; 62 | } 63 | if (!passwordUpdateBucket.consume(session.id, 1)) { 64 | return { 65 | message: "Too many requests" 66 | }; 67 | } 68 | const passwordHash = getUserPasswordHash(user.id); 69 | const validPassword = await verifyPasswordHash(passwordHash, password); 70 | if (!validPassword) { 71 | return { 72 | message: "Incorrect password" 73 | }; 74 | } 75 | passwordUpdateBucket.reset(session.id); 76 | invalidateUserSessions(user.id); 77 | await updateUserPassword(user.id, newPassword); 78 | 79 | const sessionToken = generateSessionToken(); 80 | const sessionFlags: SessionFlags = { 81 | twoFactorVerified: session.twoFactorVerified 82 | }; 83 | const newSession = createSession(sessionToken, user.id, sessionFlags); 84 | setSessionTokenCookie(sessionToken, newSession.expiresAt); 85 | return { 86 | message: "Updated password" 87 | }; 88 | } 89 | 90 | export async function updateEmailAction(_prev: ActionResult, formData: FormData): Promise { 91 | if (!globalPOSTRateLimit()) { 92 | return { 93 | message: "Too many requests" 94 | }; 95 | } 96 | const { session, user } = getCurrentSession(); 97 | if (session === null) { 98 | return { 99 | message: "Not authenticated" 100 | }; 101 | } 102 | if (user.registered2FA && !session.twoFactorVerified) { 103 | return { 104 | message: "Forbidden" 105 | }; 106 | } 107 | if (!sendVerificationEmailBucket.check(user.id, 1)) { 108 | return { 109 | message: "Too many requests" 110 | }; 111 | } 112 | 113 | const email = formData.get("email"); 114 | if (typeof email !== "string") { 115 | return { message: "Invalid or missing fields" }; 116 | } 117 | if (email === "") { 118 | return { 119 | message: "Please enter your email" 120 | }; 121 | } 122 | if (!verifyEmailInput(email)) { 123 | return { 124 | message: "Please enter a valid email" 125 | }; 126 | } 127 | const emailAvailable = checkEmailAvailability(email); 128 | if (!emailAvailable) { 129 | return { 130 | message: "This email is already used" 131 | }; 132 | } 133 | if (!sendVerificationEmailBucket.consume(user.id, 1)) { 134 | return { 135 | message: "Too many requests" 136 | }; 137 | } 138 | const verificationRequest = createEmailVerificationRequest(user.id, email); 139 | sendVerificationEmail(verificationRequest.email, verificationRequest.code); 140 | setEmailVerificationRequestCookie(verificationRequest); 141 | return redirect("/verify-email"); 142 | } 143 | 144 | export async function regenerateRecoveryCodeAction(): Promise { 145 | if (!globalPOSTRateLimit()) { 146 | return { 147 | error: "Too many requests", 148 | recoveryCode: null 149 | }; 150 | } 151 | const { session, user } = getCurrentSession(); 152 | if (session === null || user === null) { 153 | return { 154 | error: "Not authenticated", 155 | recoveryCode: null 156 | }; 157 | } 158 | if (!user.emailVerified) { 159 | return { 160 | error: "Forbidden", 161 | recoveryCode: null 162 | }; 163 | } 164 | if (!session.twoFactorVerified) { 165 | return { 166 | error: "Forbidden", 167 | recoveryCode: null 168 | }; 169 | } 170 | const recoveryCode = resetUserRecoveryCode(session.userId); 171 | return { 172 | error: null, 173 | recoveryCode 174 | }; 175 | } 176 | 177 | interface ActionResult { 178 | message: string; 179 | } 180 | 181 | type RegenerateRecoveryCodeActionResult = 182 | | { 183 | error: string; 184 | recoveryCode: null; 185 | } 186 | | { 187 | error: null; 188 | recoveryCode: string; 189 | }; 190 | -------------------------------------------------------------------------------- /app/settings/components.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { regenerateRecoveryCodeAction, updateEmailAction, updatePasswordAction } from "./actions"; 5 | import { useFormState } from "react-dom"; 6 | 7 | const initialUpdatePasswordState = { 8 | message: "" 9 | }; 10 | 11 | export function UpdatePasswordForm() { 12 | const [state, action] = useFormState(updatePasswordAction, initialUpdatePasswordState); 13 | 14 | return ( 15 |
16 | 17 | 18 |
19 | 20 | 21 |
22 | 23 |

{state.message}

24 |
25 | ); 26 | } 27 | 28 | const initialUpdateFormState = { 29 | message: "" 30 | }; 31 | 32 | export function UpdateEmailForm() { 33 | const [state, action] = useFormState(updateEmailAction, initialUpdateFormState); 34 | 35 | return ( 36 |
37 | 38 | 39 |
40 | 41 |

{state.message}

42 |
43 | ); 44 | } 45 | 46 | export function RecoveryCodeSection(props: { recoveryCode: string }) { 47 | const [recoveryCode, setRecoveryCode] = useState(props.recoveryCode); 48 | return ( 49 |
50 |

Recovery code

51 |

Your recovery code is: {recoveryCode}

52 | 62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /app/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { RecoveryCodeSection, UpdateEmailForm, UpdatePasswordForm } from "./components"; 4 | import { getCurrentSession } from "@/lib/server/session"; 5 | import { redirect } from "next/navigation"; 6 | import { getUserRecoverCode } from "@/lib/server/user"; 7 | import { globalGETRateLimit } from "@/lib/server/request"; 8 | 9 | export default function Page() { 10 | if (!globalGETRateLimit()) { 11 | return "Too many requests"; 12 | } 13 | const { session, user } = getCurrentSession(); 14 | if (session === null) { 15 | return redirect("/login"); 16 | } 17 | if (user.registered2FA && !session.twoFactorVerified) { 18 | return redirect("/2fa"); 19 | } 20 | let recoveryCode: string | null = null; 21 | if (user.registered2FA) { 22 | recoveryCode = getUserRecoverCode(user.id); 23 | } 24 | return ( 25 | <> 26 |
27 | Home 28 | Settings 29 |
30 |
31 |

Settings

32 |
33 |

Update email

34 |

Your email: {user.email}

35 | 36 |
37 |
38 |

Update password

39 | 40 |
41 | {user.registered2FA && ( 42 |
43 |

Update two-factor authentication

44 | Update 45 |
46 | )} 47 | {recoveryCode !== null && } 48 |
49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /app/signup/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { checkEmailAvailability, verifyEmailInput } from "@/lib/server/email"; 4 | import { 5 | createEmailVerificationRequest, 6 | sendVerificationEmail, 7 | setEmailVerificationRequestCookie 8 | } from "@/lib/server/email-verification"; 9 | import { verifyPasswordStrength } from "@/lib/server/password"; 10 | import { RefillingTokenBucket } from "@/lib/server/rate-limit"; 11 | import { createSession, generateSessionToken, setSessionTokenCookie } from "@/lib/server/session"; 12 | import { createUser, verifyUsernameInput } from "@/lib/server/user"; 13 | import { headers } from "next/headers"; 14 | import { redirect } from "next/navigation"; 15 | import { globalPOSTRateLimit } from "@/lib/server/request"; 16 | 17 | import type { SessionFlags } from "@/lib/server/session"; 18 | 19 | const ipBucket = new RefillingTokenBucket(3, 10); 20 | 21 | export async function signupAction(_prev: ActionResult, formData: FormData): Promise { 22 | if (!globalPOSTRateLimit()) { 23 | return { 24 | message: "Too many requests" 25 | }; 26 | } 27 | 28 | // TODO: Assumes X-Forwarded-For is always included. 29 | const clientIP = headers().get("X-Forwarded-For"); 30 | if (clientIP !== null && !ipBucket.check(clientIP, 1)) { 31 | return { 32 | message: "Too many requests" 33 | }; 34 | } 35 | 36 | const email = formData.get("email"); 37 | const username = formData.get("username"); 38 | const password = formData.get("password"); 39 | if (typeof email !== "string" || typeof username !== "string" || typeof password !== "string") { 40 | return { 41 | message: "Invalid or missing fields" 42 | }; 43 | } 44 | if (email === "" || password === "" || username === "") { 45 | return { 46 | message: "Please enter your username, email, and password" 47 | }; 48 | } 49 | if (!verifyEmailInput(email)) { 50 | return { 51 | message: "Invalid email" 52 | }; 53 | } 54 | const emailAvailable = checkEmailAvailability(email); 55 | if (!emailAvailable) { 56 | return { 57 | message: "Email is already used" 58 | }; 59 | } 60 | if (!verifyUsernameInput(username)) { 61 | return { 62 | message: "Invalid username" 63 | }; 64 | } 65 | const strongPassword = await verifyPasswordStrength(password); 66 | if (!strongPassword) { 67 | return { 68 | message: "Weak password" 69 | }; 70 | } 71 | if (clientIP !== null && !ipBucket.consume(clientIP, 1)) { 72 | return { 73 | message: "Too many requests" 74 | }; 75 | } 76 | const user = await createUser(email, username, password); 77 | const emailVerificationRequest = createEmailVerificationRequest(user.id, user.email); 78 | sendVerificationEmail(emailVerificationRequest.email, emailVerificationRequest.code); 79 | setEmailVerificationRequestCookie(emailVerificationRequest); 80 | 81 | const sessionFlags: SessionFlags = { 82 | twoFactorVerified: false 83 | }; 84 | const sessionToken = generateSessionToken(); 85 | const session = createSession(sessionToken, user.id, sessionFlags); 86 | setSessionTokenCookie(sessionToken, session.expiresAt); 87 | return redirect("/2fa/setup"); 88 | } 89 | 90 | interface ActionResult { 91 | message: string; 92 | } 93 | -------------------------------------------------------------------------------- /app/signup/components.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { signupAction } from "./actions"; 4 | import { useFormState } from "react-dom"; 5 | 6 | const initialState = { 7 | message: "" 8 | }; 9 | 10 | export function SignUpForm() { 11 | const [state, action] = useFormState(signupAction, initialState); 12 | 13 | return ( 14 |
15 | 16 | 17 |
18 | 19 | 20 |
21 | 22 | 23 |
24 | 25 |

{state.message}

26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /app/signup/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUpForm } from "./components"; 2 | import Link from "next/link"; 3 | 4 | import { getCurrentSession } from "@/lib/server/session"; 5 | import { redirect } from "next/navigation"; 6 | import { globalGETRateLimit } from "@/lib/server/request"; 7 | 8 | export default function Page() { 9 | if (!globalGETRateLimit()) { 10 | return "Too many requests"; 11 | } 12 | const { session, user } = getCurrentSession(); 13 | if (session !== null) { 14 | if (!user.emailVerified) { 15 | return redirect("/verify-email"); 16 | } 17 | if (!user.registered2FA) { 18 | return redirect("/2fa/setup"); 19 | } 20 | if (!session.twoFactorVerified) { 21 | return redirect("/2fa"); 22 | } 23 | return redirect("/"); 24 | } 25 | return ( 26 | <> 27 |

Create an account

28 |

Your username must be at least 3 characters long and your password must be at least 8 characters long.

29 | 30 | Sign in 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /app/verify-email/actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { 4 | createEmailVerificationRequest, 5 | deleteEmailVerificationRequestCookie, 6 | deleteUserEmailVerificationRequest, 7 | getUserEmailVerificationRequestFromRequest, 8 | sendVerificationEmail, 9 | sendVerificationEmailBucket, 10 | setEmailVerificationRequestCookie 11 | } from "@/lib/server/email-verification"; 12 | import { invalidateUserPasswordResetSessions } from "@/lib/server/password-reset"; 13 | import { ExpiringTokenBucket } from "@/lib/server/rate-limit"; 14 | import { globalPOSTRateLimit } from "@/lib/server/request"; 15 | import { getCurrentSession } from "@/lib/server/session"; 16 | import { updateUserEmailAndSetEmailAsVerified } from "@/lib/server/user"; 17 | import { redirect } from "next/navigation"; 18 | 19 | const bucket = new ExpiringTokenBucket(5, 60 * 30); 20 | 21 | export async function verifyEmailAction(_prev: ActionResult, formData: FormData): Promise { 22 | if (!globalPOSTRateLimit()) { 23 | return { 24 | message: "Too many requests" 25 | }; 26 | } 27 | 28 | const { session, user } = getCurrentSession(); 29 | if (session === null) { 30 | return { 31 | message: "Not authenticated" 32 | }; 33 | } 34 | if (user.registered2FA && !session.twoFactorVerified) { 35 | return { 36 | message: "Forbidden" 37 | }; 38 | } 39 | if (!bucket.check(user.id, 1)) { 40 | return { 41 | message: "Too many requests" 42 | }; 43 | } 44 | 45 | let verificationRequest = getUserEmailVerificationRequestFromRequest(); 46 | if (verificationRequest === null) { 47 | return { 48 | message: "Not authenticated" 49 | }; 50 | } 51 | const code = formData.get("code"); 52 | if (typeof code !== "string") { 53 | return { 54 | message: "Invalid or missing fields" 55 | }; 56 | } 57 | if (code === "") { 58 | return { 59 | message: "Enter your code" 60 | }; 61 | } 62 | if (!bucket.consume(user.id, 1)) { 63 | return { 64 | message: "Too many requests" 65 | }; 66 | } 67 | if (Date.now() >= verificationRequest.expiresAt.getTime()) { 68 | verificationRequest = createEmailVerificationRequest(verificationRequest.userId, verificationRequest.email); 69 | sendVerificationEmail(verificationRequest.email, verificationRequest.code); 70 | return { 71 | message: "The verification code was expired. We sent another code to your inbox." 72 | }; 73 | } 74 | if (verificationRequest.code !== code) { 75 | return { 76 | message: "Incorrect code." 77 | }; 78 | } 79 | deleteUserEmailVerificationRequest(user.id); 80 | invalidateUserPasswordResetSessions(user.id); 81 | updateUserEmailAndSetEmailAsVerified(user.id, verificationRequest.email); 82 | deleteEmailVerificationRequestCookie(); 83 | if (!user.registered2FA) { 84 | return redirect("/2fa/setup"); 85 | } 86 | return redirect("/"); 87 | } 88 | 89 | export async function resendEmailVerificationCodeAction(): Promise { 90 | const { session, user } = getCurrentSession(); 91 | if (session === null) { 92 | return { 93 | message: "Not authenticated" 94 | }; 95 | } 96 | if (user.registered2FA && !session.twoFactorVerified) { 97 | return { 98 | message: "Forbidden" 99 | }; 100 | } 101 | if (!sendVerificationEmailBucket.check(user.id, 1)) { 102 | return { 103 | message: "Too many requests" 104 | }; 105 | } 106 | let verificationRequest = getUserEmailVerificationRequestFromRequest(); 107 | if (verificationRequest === null) { 108 | if (user.emailVerified) { 109 | return { 110 | message: "Forbidden" 111 | }; 112 | } 113 | if (!sendVerificationEmailBucket.consume(user.id, 1)) { 114 | return { 115 | message: "Too many requests" 116 | }; 117 | } 118 | verificationRequest = createEmailVerificationRequest(user.id, user.email); 119 | } else { 120 | if (!sendVerificationEmailBucket.consume(user.id, 1)) { 121 | return { 122 | message: "Too many requests" 123 | }; 124 | } 125 | verificationRequest = createEmailVerificationRequest(user.id, verificationRequest.email); 126 | } 127 | sendVerificationEmail(verificationRequest.email, verificationRequest.code); 128 | setEmailVerificationRequestCookie(verificationRequest); 129 | return { 130 | message: "A new code was sent to your inbox." 131 | }; 132 | } 133 | 134 | interface ActionResult { 135 | message: string; 136 | } 137 | -------------------------------------------------------------------------------- /app/verify-email/components.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { resendEmailVerificationCodeAction, verifyEmailAction } from "./actions"; 4 | import { useFormState } from "react-dom"; 5 | 6 | const emailVerificationInitialState = { 7 | message: "" 8 | }; 9 | 10 | export function EmailVerificationForm() { 11 | const [state, action] = useFormState(verifyEmailAction, emailVerificationInitialState); 12 | return ( 13 |
14 | 15 | 16 | 17 |

{state.message}

18 |
19 | ); 20 | } 21 | 22 | const resendEmailInitialState = { 23 | message: "" 24 | }; 25 | 26 | export function ResendEmailVerificationCodeForm() { 27 | const [state, action] = useFormState(resendEmailVerificationCodeAction, resendEmailInitialState); 28 | return ( 29 |
30 | 31 |

{state.message}

32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /app/verify-email/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { EmailVerificationForm, ResendEmailVerificationCodeForm } from "./components"; 3 | 4 | import { getCurrentSession } from "@/lib/server/session"; 5 | import { redirect } from "next/navigation"; 6 | import { getUserEmailVerificationRequestFromRequest } from "@/lib/server/email-verification"; 7 | import { globalGETRateLimit } from "@/lib/server/request"; 8 | 9 | export default function Page() { 10 | if (!globalGETRateLimit()) { 11 | return "Too many requests"; 12 | } 13 | const { user } = getCurrentSession(); 14 | if (user === null) { 15 | return redirect("/login"); 16 | } 17 | 18 | // TODO: Ideally we'd sent a new verification email automatically if the previous one is expired, 19 | // but we can't set cookies inside server components. 20 | const verificationRequest = getUserEmailVerificationRequestFromRequest(); 21 | if (verificationRequest === null && user.emailVerified) { 22 | return redirect("/"); 23 | } 24 | return ( 25 | <> 26 |

Verify your email address

27 |

We sent an 8-digit code to {verificationRequest?.email ?? user.email}.

28 | 29 | 30 | Change your email 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /lib/server/2fa.ts: -------------------------------------------------------------------------------- 1 | import { db } from "./db"; 2 | import { decryptToString, encryptString } from "./encryption"; 3 | import { ExpiringTokenBucket } from "./rate-limit"; 4 | import { generateRandomRecoveryCode } from "./utils"; 5 | 6 | export const totpBucket = new ExpiringTokenBucket(5, 60 * 30); 7 | export const recoveryCodeBucket = new ExpiringTokenBucket(3, 60 * 60); 8 | 9 | export function resetUser2FAWithRecoveryCode(userId: number, recoveryCode: string): boolean { 10 | // Note: In Postgres and MySQL, these queries should be done in a transaction using SELECT FOR UPDATE 11 | const row = db.queryOne("SELECT recovery_code FROM user WHERE id = ?", [userId]); 12 | if (row === null) { 13 | return false; 14 | } 15 | const encryptedRecoveryCode = row.bytes(0); 16 | const userRecoveryCode = decryptToString(encryptedRecoveryCode); 17 | if (recoveryCode !== userRecoveryCode) { 18 | return false; 19 | } 20 | 21 | const newRecoveryCode = generateRandomRecoveryCode(); 22 | const encryptedNewRecoveryCode = encryptString(newRecoveryCode); 23 | db.execute("UPDATE session SET two_factor_verified = 0 WHERE user_id = ?", [userId]); 24 | // Compare old recovery code to ensure recovery code wasn't updated. 25 | const result = db.execute("UPDATE user SET recovery_code = ?, totp_key = NULL WHERE id = ? AND recovery_code = ?", [ 26 | encryptedNewRecoveryCode, 27 | userId, 28 | encryptedRecoveryCode 29 | ]); 30 | return result.changes > 0; 31 | } 32 | -------------------------------------------------------------------------------- /lib/server/db.ts: -------------------------------------------------------------------------------- 1 | import sqlite3 from "better-sqlite3"; 2 | import { SyncDatabase } from "@pilcrowjs/db-query"; 3 | 4 | import type { SyncAdapter } from "@pilcrowjs/db-query"; 5 | 6 | const sqlite = sqlite3("sqlite.db"); 7 | 8 | const adapter: SyncAdapter = { 9 | query: (statement: string, params: unknown[]): unknown[][] => { 10 | const result = sqlite 11 | .prepare(statement) 12 | .raw() 13 | .all(...params); 14 | return result as unknown[][]; 15 | }, 16 | execute: (statement: string, params: unknown[]): sqlite3.RunResult => { 17 | const result = sqlite.prepare(statement).run(...params); 18 | return result; 19 | } 20 | }; 21 | 22 | class Database extends SyncDatabase { 23 | public inTransaction(): boolean { 24 | return sqlite.inTransaction; 25 | } 26 | } 27 | 28 | export const db = new Database(adapter); 29 | -------------------------------------------------------------------------------- /lib/server/email-verification.ts: -------------------------------------------------------------------------------- 1 | import { generateRandomOTP } from "./utils"; 2 | import { db } from "./db"; 3 | import { ExpiringTokenBucket } from "./rate-limit"; 4 | import { encodeBase32 } from "@oslojs/encoding"; 5 | import { cookies } from "next/headers"; 6 | import { getCurrentSession } from "./session"; 7 | 8 | export function getUserEmailVerificationRequest(userId: number, id: string): EmailVerificationRequest | null { 9 | const row = db.queryOne( 10 | "SELECT id, user_id, code, email, expires_at FROM email_verification_request WHERE id = ? AND user_id = ?", 11 | [id, userId] 12 | ); 13 | if (row === null) { 14 | return row; 15 | } 16 | const request: EmailVerificationRequest = { 17 | id: row.string(0), 18 | userId: row.number(1), 19 | code: row.string(2), 20 | email: row.string(3), 21 | expiresAt: new Date(row.number(4) * 1000) 22 | }; 23 | return request; 24 | } 25 | 26 | export function createEmailVerificationRequest(userId: number, email: string): EmailVerificationRequest { 27 | deleteUserEmailVerificationRequest(userId); 28 | const idBytes = new Uint8Array(20); 29 | crypto.getRandomValues(idBytes); 30 | const id = encodeBase32(idBytes).toLowerCase(); 31 | 32 | const code = generateRandomOTP(); 33 | const expiresAt = new Date(Date.now() + 1000 * 60 * 10); 34 | db.queryOne( 35 | "INSERT INTO email_verification_request (id, user_id, code, email, expires_at) VALUES (?, ?, ?, ?, ?) RETURNING id", 36 | [id, userId, code, email, Math.floor(expiresAt.getTime() / 1000)] 37 | ); 38 | 39 | const request: EmailVerificationRequest = { 40 | id, 41 | userId, 42 | code, 43 | email, 44 | expiresAt 45 | }; 46 | return request; 47 | } 48 | 49 | export function deleteUserEmailVerificationRequest(userId: number): void { 50 | db.execute("DELETE FROM email_verification_request WHERE user_id = ?", [userId]); 51 | } 52 | 53 | export function sendVerificationEmail(email: string, code: string): void { 54 | console.log(`To ${email}: Your verification code is ${code}`); 55 | } 56 | 57 | export function setEmailVerificationRequestCookie(request: EmailVerificationRequest): void { 58 | cookies().set("email_verification", request.id, { 59 | httpOnly: true, 60 | path: "/", 61 | secure: process.env.NODE_ENV === "production", 62 | sameSite: "lax", 63 | expires: request.expiresAt 64 | }); 65 | } 66 | 67 | export function deleteEmailVerificationRequestCookie(): void { 68 | cookies().set("email_verification", "", { 69 | httpOnly: true, 70 | path: "/", 71 | secure: process.env.NODE_ENV === "production", 72 | sameSite: "lax", 73 | maxAge: 0 74 | }); 75 | } 76 | 77 | export function getUserEmailVerificationRequestFromRequest(): EmailVerificationRequest | null { 78 | const { user } = getCurrentSession(); 79 | if (user === null) { 80 | return null; 81 | } 82 | const id = cookies().get("email_verification")?.value ?? null; 83 | if (id === null) { 84 | return null; 85 | } 86 | const request = getUserEmailVerificationRequest(user.id, id); 87 | if (request === null) { 88 | deleteEmailVerificationRequestCookie(); 89 | } 90 | return request; 91 | } 92 | 93 | export const sendVerificationEmailBucket = new ExpiringTokenBucket(3, 60 * 10); 94 | 95 | export interface EmailVerificationRequest { 96 | id: string; 97 | userId: number; 98 | code: string; 99 | email: string; 100 | expiresAt: Date; 101 | } 102 | -------------------------------------------------------------------------------- /lib/server/email.ts: -------------------------------------------------------------------------------- 1 | import { db } from "./db"; 2 | 3 | export function verifyEmailInput(email: string): boolean { 4 | return /^.+@.+\..+$/.test(email) && email.length < 256; 5 | } 6 | 7 | export function checkEmailAvailability(email: string): boolean { 8 | const row = db.queryOne("SELECT COUNT(*) FROM user WHERE email = ?", [email]); 9 | if (row === null) { 10 | throw new Error(); 11 | } 12 | return row.number(0) === 0; 13 | } 14 | -------------------------------------------------------------------------------- /lib/server/encryption.ts: -------------------------------------------------------------------------------- 1 | import { decodeBase64 } from "@oslojs/encoding"; 2 | import { createCipheriv, createDecipheriv } from "crypto"; 3 | import { DynamicBuffer } from "@oslojs/binary"; 4 | 5 | const key = decodeBase64(process.env.ENCRYPTION_KEY ?? ""); 6 | 7 | export function encrypt(data: Uint8Array): Uint8Array { 8 | const iv = new Uint8Array(16); 9 | crypto.getRandomValues(iv); 10 | const cipher = createCipheriv("aes-128-gcm", key, iv); 11 | const encrypted = new DynamicBuffer(0); 12 | encrypted.write(iv); 13 | encrypted.write(cipher.update(data)); 14 | encrypted.write(cipher.final()); 15 | encrypted.write(cipher.getAuthTag()); 16 | return encrypted.bytes(); 17 | } 18 | 19 | export function encryptString(data: string): Uint8Array { 20 | return encrypt(new TextEncoder().encode(data)); 21 | } 22 | 23 | export function decrypt(encrypted: Uint8Array): Uint8Array { 24 | if (encrypted.byteLength < 33) { 25 | throw new Error("Invalid data"); 26 | } 27 | const decipher = createDecipheriv("aes-128-gcm", key, encrypted.slice(0, 16)); 28 | decipher.setAuthTag(encrypted.slice(encrypted.byteLength - 16)); 29 | const decrypted = new DynamicBuffer(0); 30 | decrypted.write(decipher.update(encrypted.slice(16, encrypted.byteLength - 16))); 31 | decrypted.write(decipher.final()); 32 | return decrypted.bytes(); 33 | } 34 | 35 | export function decryptToString(data: Uint8Array): string { 36 | return new TextDecoder().decode(decrypt(data)); 37 | } 38 | -------------------------------------------------------------------------------- /lib/server/password-reset.ts: -------------------------------------------------------------------------------- 1 | import { db } from "./db"; 2 | import { encodeHexLowerCase } from "@oslojs/encoding"; 3 | import { generateRandomOTP } from "./utils"; 4 | import { sha256 } from "@oslojs/crypto/sha2"; 5 | import { cookies } from "next/headers"; 6 | 7 | import type { User } from "./user"; 8 | 9 | export function createPasswordResetSession(token: string, userId: number, email: string): PasswordResetSession { 10 | const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); 11 | const session: PasswordResetSession = { 12 | id: sessionId, 13 | userId, 14 | email, 15 | expiresAt: new Date(Date.now() + 1000 * 60 * 10), 16 | code: generateRandomOTP(), 17 | emailVerified: false, 18 | twoFactorVerified: false 19 | }; 20 | db.execute("INSERT INTO password_reset_session (id, user_id, email, code, expires_at) VALUES (?, ?, ?, ?, ?)", [ 21 | session.id, 22 | session.userId, 23 | session.email, 24 | session.code, 25 | Math.floor(session.expiresAt.getTime() / 1000) 26 | ]); 27 | return session; 28 | } 29 | 30 | export function validatePasswordResetSessionToken(token: string): PasswordResetSessionValidationResult { 31 | const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); 32 | const row = db.queryOne( 33 | `SELECT password_reset_session.id, password_reset_session.user_id, password_reset_session.email, password_reset_session.code, password_reset_session.expires_at, password_reset_session.email_verified, password_reset_session.two_factor_verified, 34 | user.id, user.email, user.username, user.email_verified, IIF(user.totp_key IS NOT NULL, 1, 0) 35 | FROM password_reset_session INNER JOIN user ON user.id = password_reset_session.user_id 36 | WHERE password_reset_session.id = ?`, 37 | [sessionId] 38 | ); 39 | if (row === null) { 40 | return { session: null, user: null }; 41 | } 42 | const session: PasswordResetSession = { 43 | id: row.string(0), 44 | userId: row.number(1), 45 | email: row.string(2), 46 | code: row.string(3), 47 | expiresAt: new Date(row.number(4) * 1000), 48 | emailVerified: Boolean(row.number(5)), 49 | twoFactorVerified: Boolean(row.number(6)) 50 | }; 51 | const user: User = { 52 | id: row.number(7), 53 | email: row.string(8), 54 | username: row.string(9), 55 | emailVerified: Boolean(row.number(10)), 56 | registered2FA: Boolean(row.number(11)) 57 | }; 58 | if (Date.now() >= session.expiresAt.getTime()) { 59 | db.execute("DELETE FROM password_reset_session WHERE id = ?", [session.id]); 60 | return { session: null, user: null }; 61 | } 62 | return { session, user }; 63 | } 64 | 65 | export function setPasswordResetSessionAsEmailVerified(sessionId: string): void { 66 | db.execute("UPDATE password_reset_session SET email_verified = 1 WHERE id = ?", [sessionId]); 67 | } 68 | 69 | export function setPasswordResetSessionAs2FAVerified(sessionId: string): void { 70 | db.execute("UPDATE password_reset_session SET two_factor_verified = 1 WHERE id = ?", [sessionId]); 71 | } 72 | 73 | export function invalidateUserPasswordResetSessions(userId: number): void { 74 | db.execute("DELETE FROM password_reset_session WHERE user_id = ?", [userId]); 75 | } 76 | 77 | export function validatePasswordResetSessionRequest(): PasswordResetSessionValidationResult { 78 | const token = cookies().get("password_reset_session")?.value ?? null; 79 | if (token === null) { 80 | return { session: null, user: null }; 81 | } 82 | const result = validatePasswordResetSessionToken(token); 83 | if (result.session === null) { 84 | deletePasswordResetSessionTokenCookie(); 85 | } 86 | return result; 87 | } 88 | 89 | export function setPasswordResetSessionTokenCookie(token: string, expiresAt: Date): void { 90 | cookies().set("password_reset_session", token, { 91 | expires: expiresAt, 92 | sameSite: "lax", 93 | httpOnly: true, 94 | path: "/", 95 | secure: process.env.NODE_ENV === "production" 96 | }); 97 | } 98 | 99 | export function deletePasswordResetSessionTokenCookie(): void { 100 | cookies().set("password_reset_session", "", { 101 | maxAge: 0, 102 | sameSite: "lax", 103 | httpOnly: true, 104 | path: "/", 105 | secure: process.env.NODE_ENV === "production" 106 | }); 107 | } 108 | 109 | export function sendPasswordResetEmail(email: string, code: string): void { 110 | console.log(`To ${email}: Your reset code is ${code}`); 111 | } 112 | 113 | export interface PasswordResetSession { 114 | id: string; 115 | userId: number; 116 | email: string; 117 | expiresAt: Date; 118 | code: string; 119 | emailVerified: boolean; 120 | twoFactorVerified: boolean; 121 | } 122 | 123 | export type PasswordResetSessionValidationResult = 124 | | { session: PasswordResetSession; user: User } 125 | | { session: null; user: null }; 126 | -------------------------------------------------------------------------------- /lib/server/password.ts: -------------------------------------------------------------------------------- 1 | import { hash, verify } from "@node-rs/argon2"; 2 | import { sha1 } from "@oslojs/crypto/sha1"; 3 | import { encodeHexLowerCase } from "@oslojs/encoding"; 4 | 5 | export async function hashPassword(password: string): Promise { 6 | return await hash(password, { 7 | memoryCost: 19456, 8 | timeCost: 2, 9 | outputLen: 32, 10 | parallelism: 1 11 | }); 12 | } 13 | 14 | export async function verifyPasswordHash(hash: string, password: string): Promise { 15 | return await verify(hash, password); 16 | } 17 | 18 | export async function verifyPasswordStrength(password: string): Promise { 19 | if (password.length < 8 || password.length > 255) { 20 | return false; 21 | } 22 | const hash = encodeHexLowerCase(sha1(new TextEncoder().encode(password))); 23 | const hashPrefix = hash.slice(0, 5); 24 | const response = await fetch(`https://api.pwnedpasswords.com/range/${hashPrefix}`); 25 | const data = await response.text(); 26 | const items = data.split("\n"); 27 | for (const item of items) { 28 | const hashSuffix = item.slice(0, 35).toLowerCase(); 29 | if (hash === hashPrefix + hashSuffix) { 30 | return false; 31 | } 32 | } 33 | return true; 34 | } 35 | -------------------------------------------------------------------------------- /lib/server/rate-limit.ts: -------------------------------------------------------------------------------- 1 | export class RefillingTokenBucket<_Key> { 2 | public max: number; 3 | public refillIntervalSeconds: number; 4 | 5 | constructor(max: number, refillIntervalSeconds: number) { 6 | this.max = max; 7 | this.refillIntervalSeconds = refillIntervalSeconds; 8 | } 9 | 10 | private storage = new Map<_Key, RefillBucket>(); 11 | 12 | public check(key: _Key, cost: number): boolean { 13 | const bucket = this.storage.get(key) ?? null; 14 | if (bucket === null) { 15 | return true; 16 | } 17 | const now = Date.now(); 18 | const refill = Math.floor((now - bucket.refilledAt) / (this.refillIntervalSeconds * 1000)); 19 | if (refill > 0) { 20 | return Math.min(bucket.count + refill, this.max) >= cost; 21 | } 22 | return bucket.count >= cost; 23 | } 24 | 25 | public consume(key: _Key, cost: number): boolean { 26 | let bucket = this.storage.get(key) ?? null; 27 | const now = Date.now(); 28 | if (bucket === null) { 29 | bucket = { 30 | count: this.max - cost, 31 | refilledAt: now 32 | }; 33 | this.storage.set(key, bucket); 34 | return true; 35 | } 36 | const refill = Math.floor((now - bucket.refilledAt) / (this.refillIntervalSeconds * 1000)); 37 | bucket.count = Math.min(bucket.count + refill, this.max); 38 | bucket.refilledAt = now; 39 | if (bucket.count < cost) { 40 | return false; 41 | } 42 | bucket.count -= cost; 43 | this.storage.set(key, bucket); 44 | return true; 45 | } 46 | } 47 | 48 | export class Throttler<_Key> { 49 | public timeoutSeconds: number[]; 50 | 51 | private storage = new Map<_Key, ThrottlingCounter>(); 52 | 53 | constructor(timeoutSeconds: number[]) { 54 | this.timeoutSeconds = timeoutSeconds; 55 | } 56 | 57 | public consume(key: _Key): boolean { 58 | let counter = this.storage.get(key) ?? null; 59 | const now = Date.now(); 60 | if (counter === null) { 61 | counter = { 62 | timeout: 0, 63 | updatedAt: now 64 | }; 65 | this.storage.set(key, counter); 66 | return true; 67 | } 68 | const allowed = now - counter.updatedAt >= this.timeoutSeconds[counter.timeout] * 1000; 69 | if (!allowed) { 70 | return false; 71 | } 72 | counter.updatedAt = now; 73 | counter.timeout = Math.min(counter.timeout + 1, this.timeoutSeconds.length - 1); 74 | this.storage.set(key, counter); 75 | return true; 76 | } 77 | 78 | public reset(key: _Key): void { 79 | this.storage.delete(key); 80 | } 81 | } 82 | 83 | export class ExpiringTokenBucket<_Key> { 84 | public max: number; 85 | public expiresInSeconds: number; 86 | 87 | private storage = new Map<_Key, ExpiringBucket>(); 88 | 89 | constructor(max: number, expiresInSeconds: number) { 90 | this.max = max; 91 | this.expiresInSeconds = expiresInSeconds; 92 | } 93 | 94 | public check(key: _Key, cost: number): boolean { 95 | const bucket = this.storage.get(key) ?? null; 96 | const now = Date.now(); 97 | if (bucket === null) { 98 | return true; 99 | } 100 | if (now - bucket.createdAt >= this.expiresInSeconds * 1000) { 101 | return true; 102 | } 103 | return bucket.count >= cost; 104 | } 105 | 106 | public consume(key: _Key, cost: number): boolean { 107 | let bucket = this.storage.get(key) ?? null; 108 | const now = Date.now(); 109 | if (bucket === null) { 110 | bucket = { 111 | count: this.max - cost, 112 | createdAt: now 113 | }; 114 | this.storage.set(key, bucket); 115 | return true; 116 | } 117 | if (now - bucket.createdAt >= this.expiresInSeconds * 1000) { 118 | bucket.count = this.max; 119 | } 120 | if (bucket.count < cost) { 121 | return false; 122 | } 123 | bucket.count -= cost; 124 | this.storage.set(key, bucket); 125 | return true; 126 | } 127 | 128 | public reset(key: _Key): void { 129 | this.storage.delete(key); 130 | } 131 | } 132 | 133 | interface RefillBucket { 134 | count: number; 135 | refilledAt: number; 136 | } 137 | 138 | interface ExpiringBucket { 139 | count: number; 140 | createdAt: number; 141 | } 142 | 143 | interface ThrottlingCounter { 144 | timeout: number; 145 | updatedAt: number; 146 | } 147 | -------------------------------------------------------------------------------- /lib/server/request.ts: -------------------------------------------------------------------------------- 1 | import { headers } from "next/headers"; 2 | import { RefillingTokenBucket } from "./rate-limit"; 3 | 4 | export const globalBucket = new RefillingTokenBucket(100, 1); 5 | 6 | export function globalGETRateLimit(): boolean { 7 | // Note: Assumes X-Forwarded-For will always be defined. 8 | const clientIP = headers().get("X-Forwarded-For"); 9 | if (clientIP === null) { 10 | return true; 11 | } 12 | return globalBucket.consume(clientIP, 1); 13 | } 14 | 15 | export function globalPOSTRateLimit(): boolean { 16 | // Note: Assumes X-Forwarded-For will always be defined. 17 | const clientIP = headers().get("X-Forwarded-For"); 18 | if (clientIP === null) { 19 | return true; 20 | } 21 | return globalBucket.consume(clientIP, 3); 22 | } 23 | -------------------------------------------------------------------------------- /lib/server/session.ts: -------------------------------------------------------------------------------- 1 | import { db } from "./db"; 2 | import { encodeBase32LowerCaseNoPadding, encodeHexLowerCase } from "@oslojs/encoding"; 3 | import { sha256 } from "@oslojs/crypto/sha2"; 4 | import { cookies } from "next/headers"; 5 | import { cache } from "react"; 6 | 7 | import type { User } from "./user"; 8 | 9 | export function validateSessionToken(token: string): SessionValidationResult { 10 | const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); 11 | const row = db.queryOne( 12 | ` 13 | SELECT session.id, session.user_id, session.expires_at, session.two_factor_verified, user.id, user.email, user.username, user.email_verified, IIF(user.totp_key IS NOT NULL, 1, 0) FROM session 14 | INNER JOIN user ON session.user_id = user.id 15 | WHERE session.id = ? 16 | `, 17 | [sessionId] 18 | ); 19 | 20 | if (row === null) { 21 | return { session: null, user: null }; 22 | } 23 | const session: Session = { 24 | id: row.string(0), 25 | userId: row.number(1), 26 | expiresAt: new Date(row.number(2) * 1000), 27 | twoFactorVerified: Boolean(row.number(3)) 28 | }; 29 | const user: User = { 30 | id: row.number(4), 31 | email: row.string(5), 32 | username: row.string(6), 33 | emailVerified: Boolean(row.number(7)), 34 | registered2FA: Boolean(row.number(8)) 35 | }; 36 | if (Date.now() >= session.expiresAt.getTime()) { 37 | db.execute("DELETE FROM session WHERE id = ?", [session.id]); 38 | return { session: null, user: null }; 39 | } 40 | if (Date.now() >= session.expiresAt.getTime() - 1000 * 60 * 60 * 24 * 15) { 41 | session.expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 30); 42 | db.execute("UPDATE session SET expires_at = ? WHERE session.id = ?", [ 43 | Math.floor(session.expiresAt.getTime() / 1000), 44 | session.id 45 | ]); 46 | } 47 | return { session, user }; 48 | } 49 | 50 | export const getCurrentSession = cache((): SessionValidationResult => { 51 | const token = cookies().get("session")?.value ?? null; 52 | if (token === null) { 53 | return { session: null, user: null }; 54 | } 55 | const result = validateSessionToken(token); 56 | return result; 57 | }); 58 | 59 | export function invalidateSession(sessionId: string): void { 60 | db.execute("DELETE FROM session WHERE id = ?", [sessionId]); 61 | } 62 | 63 | export function invalidateUserSessions(userId: number): void { 64 | db.execute("DELETE FROM session WHERE user_id = ?", [userId]); 65 | } 66 | 67 | export function setSessionTokenCookie(token: string, expiresAt: Date): void { 68 | cookies().set("session", token, { 69 | httpOnly: true, 70 | path: "/", 71 | secure: process.env.NODE_ENV === "production", 72 | sameSite: "lax", 73 | expires: expiresAt 74 | }); 75 | } 76 | 77 | export function deleteSessionTokenCookie(): void { 78 | cookies().set("session", "", { 79 | httpOnly: true, 80 | path: "/", 81 | secure: process.env.NODE_ENV === "production", 82 | sameSite: "lax", 83 | maxAge: 0 84 | }); 85 | } 86 | 87 | export function generateSessionToken(): string { 88 | const tokenBytes = new Uint8Array(20); 89 | crypto.getRandomValues(tokenBytes); 90 | const token = encodeBase32LowerCaseNoPadding(tokenBytes).toLowerCase(); 91 | return token; 92 | } 93 | 94 | export function createSession(token: string, userId: number, flags: SessionFlags): Session { 95 | const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); 96 | const session: Session = { 97 | id: sessionId, 98 | userId, 99 | expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24 * 30), 100 | twoFactorVerified: flags.twoFactorVerified 101 | }; 102 | db.execute("INSERT INTO session (id, user_id, expires_at, two_factor_verified) VALUES (?, ?, ?, ?)", [ 103 | session.id, 104 | session.userId, 105 | Math.floor(session.expiresAt.getTime() / 1000), 106 | Number(session.twoFactorVerified) 107 | ]); 108 | return session; 109 | } 110 | 111 | export function setSessionAs2FAVerified(sessionId: string): void { 112 | db.execute("UPDATE session SET two_factor_verified = 1 WHERE id = ?", [sessionId]); 113 | } 114 | 115 | export interface SessionFlags { 116 | twoFactorVerified: boolean; 117 | } 118 | 119 | export interface Session extends SessionFlags { 120 | id: string; 121 | expiresAt: Date; 122 | userId: number; 123 | } 124 | 125 | type SessionValidationResult = { session: Session; user: User } | { session: null; user: null }; 126 | -------------------------------------------------------------------------------- /lib/server/user.ts: -------------------------------------------------------------------------------- 1 | import { db } from "./db"; 2 | import { decrypt, decryptToString, encrypt, encryptString } from "./encryption"; 3 | import { hashPassword } from "./password"; 4 | import { generateRandomRecoveryCode } from "./utils"; 5 | 6 | export function verifyUsernameInput(username: string): boolean { 7 | return username.length > 3 && username.length < 32 && username.trim() === username; 8 | } 9 | 10 | export async function createUser(email: string, username: string, password: string): Promise { 11 | const passwordHash = await hashPassword(password); 12 | const recoveryCode = generateRandomRecoveryCode(); 13 | const encryptedRecoveryCode = encryptString(recoveryCode); 14 | const row = db.queryOne( 15 | "INSERT INTO user (email, username, password_hash, recovery_code) VALUES (?, ?, ?, ?) RETURNING user.id", 16 | [email, username, passwordHash, encryptedRecoveryCode] 17 | ); 18 | if (row === null) { 19 | throw new Error("Unexpected error"); 20 | } 21 | const user: User = { 22 | id: row.number(0), 23 | username, 24 | email, 25 | emailVerified: false, 26 | registered2FA: false 27 | }; 28 | return user; 29 | } 30 | 31 | export async function updateUserPassword(userId: number, password: string): Promise { 32 | const passwordHash = await hashPassword(password); 33 | db.execute("UPDATE user SET password_hash = ? WHERE id = ?", [passwordHash, userId]); 34 | } 35 | 36 | export function updateUserEmailAndSetEmailAsVerified(userId: number, email: string): void { 37 | db.execute("UPDATE user SET email = ?, email_verified = 1 WHERE id = ?", [email, userId]); 38 | } 39 | 40 | export function setUserAsEmailVerifiedIfEmailMatches(userId: number, email: string): boolean { 41 | const result = db.execute("UPDATE user SET email_verified = 1 WHERE id = ? AND email = ?", [userId, email]); 42 | return result.changes > 0; 43 | } 44 | 45 | export function getUserPasswordHash(userId: number): string { 46 | const row = db.queryOne("SELECT password_hash FROM user WHERE id = ?", [userId]); 47 | if (row === null) { 48 | throw new Error("Invalid user ID"); 49 | } 50 | return row.string(0); 51 | } 52 | 53 | export function getUserRecoverCode(userId: number): string { 54 | const row = db.queryOne("SELECT recovery_code FROM user WHERE id = ?", [userId]); 55 | if (row === null) { 56 | throw new Error("Invalid user ID"); 57 | } 58 | return decryptToString(row.bytes(0)); 59 | } 60 | 61 | export function getUserTOTPKey(userId: number): Uint8Array | null { 62 | const row = db.queryOne("SELECT totp_key FROM user WHERE id = ?", [userId]); 63 | if (row === null) { 64 | throw new Error("Invalid user ID"); 65 | } 66 | const encrypted = row.bytesNullable(0); 67 | if (encrypted === null) { 68 | return null; 69 | } 70 | return decrypt(encrypted); 71 | } 72 | 73 | export function updateUserTOTPKey(userId: number, key: Uint8Array): void { 74 | const encrypted = encrypt(key); 75 | db.execute("UPDATE user SET totp_key = ? WHERE id = ?", [encrypted, userId]); 76 | } 77 | 78 | export function resetUserRecoveryCode(userId: number): string { 79 | const recoveryCode = generateRandomRecoveryCode(); 80 | const encrypted = encryptString(recoveryCode); 81 | db.execute("UPDATE user SET recovery_code = ? WHERE id = ?", [encrypted, userId]); 82 | return recoveryCode; 83 | } 84 | 85 | export function getUserFromEmail(email: string): User | null { 86 | const row = db.queryOne( 87 | "SELECT id, email, username, email_verified, IIF(totp_key IS NOT NULL, 1, 0) FROM user WHERE email = ?", 88 | [email] 89 | ); 90 | if (row === null) { 91 | return null; 92 | } 93 | const user: User = { 94 | id: row.number(0), 95 | email: row.string(1), 96 | username: row.string(2), 97 | emailVerified: Boolean(row.number(3)), 98 | registered2FA: Boolean(row.number(4)) 99 | }; 100 | return user; 101 | } 102 | 103 | export interface User { 104 | id: number; 105 | email: string; 106 | username: string; 107 | emailVerified: boolean; 108 | registered2FA: boolean; 109 | } 110 | -------------------------------------------------------------------------------- /lib/server/utils.ts: -------------------------------------------------------------------------------- 1 | import { encodeBase32UpperCaseNoPadding } from "@oslojs/encoding"; 2 | 3 | export function generateRandomOTP(): string { 4 | const bytes = new Uint8Array(5); 5 | crypto.getRandomValues(bytes); 6 | const code = encodeBase32UpperCaseNoPadding(bytes); 7 | return code; 8 | } 9 | 10 | export function generateRandomRecoveryCode(): string { 11 | const recoveryCodeBytes = new Uint8Array(10); 12 | crypto.getRandomValues(recoveryCodeBytes); 13 | const recoveryCode = encodeBase32UpperCaseNoPadding(recoveryCodeBytes); 14 | return recoveryCode; 15 | } 16 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | 3 | import type { NextRequest } from "next/server"; 4 | 5 | export async function middleware(request: NextRequest): Promise { 6 | if (request.method === "GET") { 7 | const response = NextResponse.next(); 8 | const token = request.cookies.get("session")?.value ?? null; 9 | if (token !== null) { 10 | // Only extend cookie expiration on GET requests since we can be sure 11 | // a new session wasn't set when handling the request. 12 | response.cookies.set("session", token, { 13 | path: "/", 14 | maxAge: 60 * 60 * 24 * 30, 15 | sameSite: "lax", 16 | httpOnly: true, 17 | secure: process.env.NODE_ENV === "production" 18 | }); 19 | } 20 | return response; 21 | } 22 | 23 | const originHeader = request.headers.get("Origin"); 24 | // NOTE: You may need to use `X-Forwarded-Host` instead 25 | const hostHeader = request.headers.get("Host"); 26 | if (originHeader === null || hostHeader === null) { 27 | return new NextResponse(null, { 28 | status: 403 29 | }); 30 | } 31 | let origin: URL; 32 | try { 33 | origin = new URL(originHeader); 34 | } catch { 35 | return new NextResponse(null, { 36 | status: 403 37 | }); 38 | } 39 | if (origin.host !== hostHeader) { 40 | return new NextResponse(null, { 41 | status: 403 42 | }); 43 | } 44 | return NextResponse.next(); 45 | } 46 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | serverComponentsExternalPackages: ["@node-rs/argon2"] 5 | } 6 | }; 7 | 8 | export default nextConfig; 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-nextjs-email-password-2fa", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "format": "prettier -w ." 11 | }, 12 | "dependencies": { 13 | "@node-rs/argon2": "^2.0.0", 14 | "@oslojs/binary": "^1.0.0", 15 | "@oslojs/crypto": "^1.0.1", 16 | "@oslojs/encoding": "^1.1.0", 17 | "@oslojs/otp": "^1.0.0", 18 | "@pilcrowjs/db-query": "^0.0.2", 19 | "better-sqlite3": "^11.3.0", 20 | "next": "14.2.14", 21 | "react": "^18", 22 | "react-dom": "^18", 23 | "uqr": "^0.1.2" 24 | }, 25 | "devDependencies": { 26 | "@types/better-sqlite3": "^7.6.11", 27 | "@types/node": "^20", 28 | "@types/react": "^18", 29 | "@types/react-dom": "^18", 30 | "eslint": "^8", 31 | "eslint-config-next": "14.2.14", 32 | "eslint-config-prettier": "^9.1.0", 33 | "prettier": "^3.3.3", 34 | "typescript": "^5" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: "9.0" 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | .: 9 | dependencies: 10 | "@node-rs/argon2": 11 | specifier: ^2.0.0 12 | version: 2.0.0 13 | "@oslojs/binary": 14 | specifier: ^1.0.0 15 | version: 1.0.0 16 | "@oslojs/crypto": 17 | specifier: ^1.0.1 18 | version: 1.0.1 19 | "@oslojs/encoding": 20 | specifier: ^1.1.0 21 | version: 1.1.0 22 | "@oslojs/otp": 23 | specifier: ^1.0.0 24 | version: 1.0.0 25 | "@pilcrowjs/db-query": 26 | specifier: ^0.0.2 27 | version: 0.0.2 28 | better-sqlite3: 29 | specifier: ^11.3.0 30 | version: 11.3.0 31 | cookie-es: 32 | specifier: ^1.2.2 33 | version: 1.2.2 34 | next: 35 | specifier: 14.2.14 36 | version: 14.2.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1) 37 | react: 38 | specifier: ^18 39 | version: 18.3.1 40 | react-dom: 41 | specifier: ^18 42 | version: 18.3.1(react@18.3.1) 43 | uqr: 44 | specifier: ^0.1.2 45 | version: 0.1.2 46 | devDependencies: 47 | "@types/better-sqlite3": 48 | specifier: ^7.6.11 49 | version: 7.6.11 50 | "@types/node": 51 | specifier: ^20 52 | version: 20.16.10 53 | "@types/react": 54 | specifier: ^18 55 | version: 18.3.11 56 | "@types/react-dom": 57 | specifier: ^18 58 | version: 18.3.0 59 | eslint: 60 | specifier: ^8 61 | version: 8.57.1 62 | eslint-config-next: 63 | specifier: 14.2.14 64 | version: 14.2.14(eslint@8.57.1)(typescript@5.6.2) 65 | eslint-config-prettier: 66 | specifier: ^9.1.0 67 | version: 9.1.0(eslint@8.57.1) 68 | prettier: 69 | specifier: ^3.3.3 70 | version: 3.3.3 71 | typescript: 72 | specifier: ^5 73 | version: 5.6.2 74 | 75 | packages: 76 | "@emnapi/core@1.3.0": 77 | resolution: 78 | { integrity: sha512-9hRqVlhwqBqCoToZ3hFcNVqL+uyHV06Y47ax4UB8L6XgVRqYz7MFnfessojo6+5TK89pKwJnpophwjTMOeKI9Q== } 79 | 80 | "@emnapi/runtime@1.3.0": 81 | resolution: 82 | { integrity: sha512-XMBySMuNZs3DM96xcJmLW4EfGnf+uGmFNjzpehMjuX5PLB5j87ar2Zc4e3PVeZ3I5g3tYtAqskB28manlF69Zw== } 83 | 84 | "@emnapi/wasi-threads@1.0.1": 85 | resolution: 86 | { integrity: sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw== } 87 | 88 | "@eslint-community/eslint-utils@4.4.0": 89 | resolution: 90 | { integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== } 91 | engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } 92 | peerDependencies: 93 | eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 94 | 95 | "@eslint-community/regexpp@4.11.1": 96 | resolution: 97 | { integrity: sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q== } 98 | engines: { node: ^12.0.0 || ^14.0.0 || >=16.0.0 } 99 | 100 | "@eslint/eslintrc@2.1.4": 101 | resolution: 102 | { integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== } 103 | engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } 104 | 105 | "@eslint/js@8.57.1": 106 | resolution: 107 | { integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== } 108 | engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } 109 | 110 | "@humanwhocodes/config-array@0.13.0": 111 | resolution: 112 | { integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== } 113 | engines: { node: ">=10.10.0" } 114 | deprecated: Use @eslint/config-array instead 115 | 116 | "@humanwhocodes/module-importer@1.0.1": 117 | resolution: 118 | { integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== } 119 | engines: { node: ">=12.22" } 120 | 121 | "@humanwhocodes/object-schema@2.0.3": 122 | resolution: 123 | { integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== } 124 | deprecated: Use @eslint/object-schema instead 125 | 126 | "@isaacs/cliui@8.0.2": 127 | resolution: 128 | { integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== } 129 | engines: { node: ">=12" } 130 | 131 | "@napi-rs/wasm-runtime@0.2.5": 132 | resolution: 133 | { integrity: sha512-kwUxR7J9WLutBbulqg1dfOrMTwhMdXLdcGUhcbCcGwnPLt3gz19uHVdwH1syKVDbE022ZS2vZxOWflFLS0YTjw== } 134 | 135 | "@next/env@14.2.14": 136 | resolution: 137 | { integrity: sha512-/0hWQfiaD5//LvGNgc8PjvyqV50vGK0cADYzaoOOGN8fxzBn3iAiaq3S0tCRnFBldq0LVveLcxCTi41ZoYgAgg== } 138 | 139 | "@next/eslint-plugin-next@14.2.14": 140 | resolution: 141 | { integrity: sha512-kV+OsZ56xhj0rnTn6HegyTGkoa16Mxjrpk7pjWumyB2P8JVQb8S9qtkjy/ye0GnTr4JWtWG4x/2qN40lKZ3iVQ== } 142 | 143 | "@next/swc-darwin-arm64@14.2.14": 144 | resolution: 145 | { integrity: sha512-bsxbSAUodM1cjYeA4o6y7sp9wslvwjSkWw57t8DtC8Zig8aG8V6r+Yc05/9mDzLKcybb6EN85k1rJDnMKBd9Gw== } 146 | engines: { node: ">= 10" } 147 | cpu: [arm64] 148 | os: [darwin] 149 | 150 | "@next/swc-darwin-x64@14.2.14": 151 | resolution: 152 | { integrity: sha512-cC9/I+0+SK5L1k9J8CInahduTVWGMXhQoXFeNvF0uNs3Bt1Ub0Azb8JzTU9vNCr0hnaMqiWu/Z0S1hfKc3+dww== } 153 | engines: { node: ">= 10" } 154 | cpu: [x64] 155 | os: [darwin] 156 | 157 | "@next/swc-linux-arm64-gnu@14.2.14": 158 | resolution: 159 | { integrity: sha512-RMLOdA2NU4O7w1PQ3Z9ft3PxD6Htl4uB2TJpocm+4jcllHySPkFaUIFacQ3Jekcg6w+LBaFvjSPthZHiPmiAUg== } 160 | engines: { node: ">= 10" } 161 | cpu: [arm64] 162 | os: [linux] 163 | 164 | "@next/swc-linux-arm64-musl@14.2.14": 165 | resolution: 166 | { integrity: sha512-WgLOA4hT9EIP7jhlkPnvz49iSOMdZgDJVvbpb8WWzJv5wBD07M2wdJXLkDYIpZmCFfo/wPqFsFR4JS4V9KkQ2A== } 167 | engines: { node: ">= 10" } 168 | cpu: [arm64] 169 | os: [linux] 170 | 171 | "@next/swc-linux-x64-gnu@14.2.14": 172 | resolution: 173 | { integrity: sha512-lbn7svjUps1kmCettV/R9oAvEW+eUI0lo0LJNFOXoQM5NGNxloAyFRNByYeZKL3+1bF5YE0h0irIJfzXBq9Y6w== } 174 | engines: { node: ">= 10" } 175 | cpu: [x64] 176 | os: [linux] 177 | 178 | "@next/swc-linux-x64-musl@14.2.14": 179 | resolution: 180 | { integrity: sha512-7TcQCvLQ/hKfQRgjxMN4TZ2BRB0P7HwrGAYL+p+m3u3XcKTraUFerVbV3jkNZNwDeQDa8zdxkKkw2els/S5onQ== } 181 | engines: { node: ">= 10" } 182 | cpu: [x64] 183 | os: [linux] 184 | 185 | "@next/swc-win32-arm64-msvc@14.2.14": 186 | resolution: 187 | { integrity: sha512-8i0Ou5XjTLEje0oj0JiI0Xo9L/93ghFtAUYZ24jARSeTMXLUx8yFIdhS55mTExq5Tj4/dC2fJuaT4e3ySvXU1A== } 188 | engines: { node: ">= 10" } 189 | cpu: [arm64] 190 | os: [win32] 191 | 192 | "@next/swc-win32-ia32-msvc@14.2.14": 193 | resolution: 194 | { integrity: sha512-2u2XcSaDEOj+96eXpyjHjtVPLhkAFw2nlaz83EPeuK4obF+HmtDJHqgR1dZB7Gb6V/d55FL26/lYVd0TwMgcOQ== } 195 | engines: { node: ">= 10" } 196 | cpu: [ia32] 197 | os: [win32] 198 | 199 | "@next/swc-win32-x64-msvc@14.2.14": 200 | resolution: 201 | { integrity: sha512-MZom+OvZ1NZxuRovKt1ApevjiUJTcU2PmdJKL66xUPaJeRywnbGGRWUlaAOwunD6dX+pm83vj979NTC8QXjGWg== } 202 | engines: { node: ">= 10" } 203 | cpu: [x64] 204 | os: [win32] 205 | 206 | "@node-rs/argon2-android-arm-eabi@2.0.0": 207 | resolution: 208 | { integrity: sha512-PbnBBiHg/boaj9EyQ1+Y53OQrvxJh1knrPZmzr6wRTR+DyCSHnwHpBANVhXV/T4Z9MJywy1SHFeCpuUMxN6HTw== } 209 | engines: { node: ">= 10" } 210 | cpu: [arm] 211 | os: [android] 212 | 213 | "@node-rs/argon2-android-arm64@2.0.0": 214 | resolution: 215 | { integrity: sha512-o3L+m7E2TnVRFRCAlJm3TF8ZR+WyMykakCSopOYaAcAtjwlhOOiaQsV+iw2WS3ju3AuvJ/0Qu7A4ecdHAWQ4aA== } 216 | engines: { node: ">= 10" } 217 | cpu: [arm64] 218 | os: [android] 219 | 220 | "@node-rs/argon2-darwin-arm64@2.0.0": 221 | resolution: 222 | { integrity: sha512-ad5qpXGxDwIS4/kH18LNe0G4m/4tm0z7gI4DmlTU4r3/LnrbMBW7XTHHNjUGpmtgAEn+4g/WQj1fIGtJZNFerw== } 223 | engines: { node: ">= 10" } 224 | cpu: [arm64] 225 | os: [darwin] 226 | 227 | "@node-rs/argon2-darwin-x64@2.0.0": 228 | resolution: 229 | { integrity: sha512-DhWHTjO3XNf8g9CflXZHeHbPFsRxvgsqC52S15Adj0lKSrWFzxvE9+z27jAP5KE/GmfDa4Ln5Dc4qQIQEblZAA== } 230 | engines: { node: ">= 10" } 231 | cpu: [x64] 232 | os: [darwin] 233 | 234 | "@node-rs/argon2-freebsd-x64@2.0.0": 235 | resolution: 236 | { integrity: sha512-FdLOudD9D9aL/0XB4QJl5OY48+XfZZbrPrVhcpbHgskZmo/ISWTn2z/TEyjraE7Zy9D/4X8d3/WMJiXueoyZPw== } 237 | engines: { node: ">= 10" } 238 | cpu: [x64] 239 | os: [freebsd] 240 | 241 | "@node-rs/argon2-linux-arm-gnueabihf@2.0.0": 242 | resolution: 243 | { integrity: sha512-Qtp0bsVv+/dnZsLl1O7CWVqp1ijb3r2YMvQnkK3z06pH6fajMH9LWQCGdZslnidNfRudwxj51Xiym+nD8POuYw== } 244 | engines: { node: ">= 10" } 245 | cpu: [arm] 246 | os: [linux] 247 | 248 | "@node-rs/argon2-linux-arm64-gnu@2.0.0": 249 | resolution: 250 | { integrity: sha512-1maxs5vFcn3q6jgWuG92oT7uZxcPEHTCu3IIOEVaQCJzVSAfyL7h6o0hWaHj7Ga3/5rhiaDgSwX1MTW7Crl2QQ== } 251 | engines: { node: ">= 10" } 252 | cpu: [arm64] 253 | os: [linux] 254 | 255 | "@node-rs/argon2-linux-arm64-musl@2.0.0": 256 | resolution: 257 | { integrity: sha512-Wju44r1YDhQXDWxGNkfft0iHvoXcmxJ5NJSoqSMpNuHiGOkqW2heWtBlaXZk71F8rfFLZ8hES+t1uouXYV1mNw== } 258 | engines: { node: ">= 10" } 259 | cpu: [arm64] 260 | os: [linux] 261 | 262 | "@node-rs/argon2-linux-x64-gnu@2.0.0": 263 | resolution: 264 | { integrity: sha512-udCXRjNR9RU03n4NeUcrsgsVVDzWatI5RvRPererR8mCiljDzNB89hZokXtEVtVTHDey+0WVa5lL13wJdkQOAQ== } 265 | engines: { node: ">= 10" } 266 | cpu: [x64] 267 | os: [linux] 268 | 269 | "@node-rs/argon2-linux-x64-musl@2.0.0": 270 | resolution: 271 | { integrity: sha512-RZGpd82OzYkGg4ZCLgCqwZ68m5qizPUBCDRwTquaUi4JV1Wly4iJnMlQuXsvgwvX8AKbCWKRB2HfXfOxnYRCOA== } 272 | engines: { node: ">= 10" } 273 | cpu: [x64] 274 | os: [linux] 275 | 276 | "@node-rs/argon2-wasm32-wasi@2.0.0": 277 | resolution: 278 | { integrity: sha512-WUbkAsxV2ZbwfVKJ/YgSLngn1H2IJ6uO4FYw+KPkDDYbpUHPImg0ny6k2gV340qUXYP6QvHg7EorUbXrLCE8Rg== } 279 | engines: { node: ">=14.0.0" } 280 | cpu: [wasm32] 281 | 282 | "@node-rs/argon2-win32-arm64-msvc@2.0.0": 283 | resolution: 284 | { integrity: sha512-yEvArafekU2Roo3DweBReQrRIvD+VQDiLyKhY6uIi/J27iKdWUcpqK2KTyYRak2z32LwqGS+f/4OlOGIHrbFgA== } 285 | engines: { node: ">= 10" } 286 | cpu: [arm64] 287 | os: [win32] 288 | 289 | "@node-rs/argon2-win32-ia32-msvc@2.0.0": 290 | resolution: 291 | { integrity: sha512-JUxcS00LmJ1xJgb1Wc5hg/neuqHmFJXdMEejdul45Mdi5GVuv4lsy3ywXkggjlDfETDkvlgnKgoaHG0mcIdtdg== } 292 | engines: { node: ">= 10" } 293 | cpu: [ia32] 294 | os: [win32] 295 | 296 | "@node-rs/argon2-win32-x64-msvc@2.0.0": 297 | resolution: 298 | { integrity: sha512-Wp+CuqwX97y8v+TXRU7FDCeXpWDqqoEZzBTD5RTY/ncgoyhyYsToHGztoKrlK9Nc4Q5Pco9OaJsgL5piHzvzcQ== } 299 | engines: { node: ">= 10" } 300 | cpu: [x64] 301 | os: [win32] 302 | 303 | "@node-rs/argon2@2.0.0": 304 | resolution: 305 | { integrity: sha512-19DVNk87JRT0f5R9CucMM06cQg6PAoRBEfvmKXdrD0uFAWUmaf58IXTyCLdVLJW0IXwfV7I0FCldTiOilh1rZA== } 306 | engines: { node: ">= 10" } 307 | 308 | "@nodelib/fs.scandir@2.1.5": 309 | resolution: 310 | { integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== } 311 | engines: { node: ">= 8" } 312 | 313 | "@nodelib/fs.stat@2.0.5": 314 | resolution: 315 | { integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== } 316 | engines: { node: ">= 8" } 317 | 318 | "@nodelib/fs.walk@1.2.8": 319 | resolution: 320 | { integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== } 321 | engines: { node: ">= 8" } 322 | 323 | "@nolyfill/is-core-module@1.0.39": 324 | resolution: 325 | { integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA== } 326 | engines: { node: ">=12.4.0" } 327 | 328 | "@oslojs/asn1@1.0.0": 329 | resolution: 330 | { integrity: sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA== } 331 | 332 | "@oslojs/binary@1.0.0": 333 | resolution: 334 | { integrity: sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ== } 335 | 336 | "@oslojs/crypto@1.0.0": 337 | resolution: 338 | { integrity: sha512-dVz8TkkgYdr3tlwxHd7SCYGxoN7ynwHLA0nei/Aq9C+ERU0BK+U8+/3soEzBUxUNKYBf42351DyJUZ2REla50w== } 339 | 340 | "@oslojs/crypto@1.0.1": 341 | resolution: 342 | { integrity: sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ== } 343 | 344 | "@oslojs/encoding@1.0.0": 345 | resolution: 346 | { integrity: sha512-dyIB0SdZgMm5BhGwdSp8rMxEFIopLKxDG1vxIBaiogyom6ZqH2aXPb6DEC2WzOOWKdPSq1cxdNeRx2wAn1Z+ZQ== } 347 | 348 | "@oslojs/encoding@1.1.0": 349 | resolution: 350 | { integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ== } 351 | 352 | "@oslojs/otp@1.0.0": 353 | resolution: 354 | { integrity: sha512-w/vZfoVsFCCcmsmsXVsIMoWbvr1IZmQ9BsDZwdePSpe8rFKMD1Knd+05iJr415adXkFVyu0tYxgrLPYMynNtXQ== } 355 | 356 | "@pilcrowjs/db-query@0.0.2": 357 | resolution: 358 | { integrity: sha512-d1iARoIxeUL2cTGhJe4JPhp/n1sXtgnM1mL7elrfsKjdwwjWTDyPDtVcGQy6W7RvrtZ40Wh0pdeYdBnboQjewg== } 359 | 360 | "@pkgjs/parseargs@0.11.0": 361 | resolution: 362 | { integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== } 363 | engines: { node: ">=14" } 364 | 365 | "@rtsao/scc@1.1.0": 366 | resolution: 367 | { integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== } 368 | 369 | "@rushstack/eslint-patch@1.10.4": 370 | resolution: 371 | { integrity: sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA== } 372 | 373 | "@swc/counter@0.1.3": 374 | resolution: 375 | { integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== } 376 | 377 | "@swc/helpers@0.5.5": 378 | resolution: 379 | { integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A== } 380 | 381 | "@tybys/wasm-util@0.9.0": 382 | resolution: 383 | { integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw== } 384 | 385 | "@types/better-sqlite3@7.6.11": 386 | resolution: 387 | { integrity: sha512-i8KcD3PgGtGBLl3+mMYA8PdKkButvPyARxA7IQAd6qeslht13qxb1zzO8dRCtE7U3IoJS782zDBAeoKiM695kg== } 388 | 389 | "@types/json5@0.0.29": 390 | resolution: 391 | { integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== } 392 | 393 | "@types/node@20.16.10": 394 | resolution: 395 | { integrity: sha512-vQUKgWTjEIRFCvK6CyriPH3MZYiYlNy0fKiEYHWbcoWLEgs4opurGGKlebrTLqdSMIbXImH6XExNiIyNUv3WpA== } 396 | 397 | "@types/prop-types@15.7.13": 398 | resolution: 399 | { integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA== } 400 | 401 | "@types/react-dom@18.3.0": 402 | resolution: 403 | { integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg== } 404 | 405 | "@types/react@18.3.11": 406 | resolution: 407 | { integrity: sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ== } 408 | 409 | "@typescript-eslint/eslint-plugin@8.8.0": 410 | resolution: 411 | { integrity: sha512-wORFWjU30B2WJ/aXBfOm1LX9v9nyt9D3jsSOxC3cCaTQGCW5k4jNpmjFv3U7p/7s4yvdjHzwtv2Sd2dOyhjS0A== } 412 | engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } 413 | peerDependencies: 414 | "@typescript-eslint/parser": ^8.0.0 || ^8.0.0-alpha.0 415 | eslint: ^8.57.0 || ^9.0.0 416 | typescript: "*" 417 | peerDependenciesMeta: 418 | typescript: 419 | optional: true 420 | 421 | "@typescript-eslint/parser@8.8.0": 422 | resolution: 423 | { integrity: sha512-uEFUsgR+tl8GmzmLjRqz+VrDv4eoaMqMXW7ruXfgThaAShO9JTciKpEsB+TvnfFfbg5IpujgMXVV36gOJRLtZg== } 424 | engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } 425 | peerDependencies: 426 | eslint: ^8.57.0 || ^9.0.0 427 | typescript: "*" 428 | peerDependenciesMeta: 429 | typescript: 430 | optional: true 431 | 432 | "@typescript-eslint/scope-manager@8.8.0": 433 | resolution: 434 | { integrity: sha512-EL8eaGC6gx3jDd8GwEFEV091210U97J0jeEHrAYvIYosmEGet4wJ+g0SYmLu+oRiAwbSA5AVrt6DxLHfdd+bUg== } 435 | engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } 436 | 437 | "@typescript-eslint/type-utils@8.8.0": 438 | resolution: 439 | { integrity: sha512-IKwJSS7bCqyCeG4NVGxnOP6lLT9Okc3Zj8hLO96bpMkJab+10HIfJbMouLrlpyOr3yrQ1cA413YPFiGd1mW9/Q== } 440 | engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } 441 | peerDependencies: 442 | typescript: "*" 443 | peerDependenciesMeta: 444 | typescript: 445 | optional: true 446 | 447 | "@typescript-eslint/types@8.8.0": 448 | resolution: 449 | { integrity: sha512-QJwc50hRCgBd/k12sTykOJbESe1RrzmX6COk8Y525C9l7oweZ+1lw9JiU56im7Amm8swlz00DRIlxMYLizr2Vw== } 450 | engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } 451 | 452 | "@typescript-eslint/typescript-estree@8.8.0": 453 | resolution: 454 | { integrity: sha512-ZaMJwc/0ckLz5DaAZ+pNLmHv8AMVGtfWxZe/x2JVEkD5LnmhWiQMMcYT7IY7gkdJuzJ9P14fRy28lUrlDSWYdw== } 455 | engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } 456 | peerDependencies: 457 | typescript: "*" 458 | peerDependenciesMeta: 459 | typescript: 460 | optional: true 461 | 462 | "@typescript-eslint/utils@8.8.0": 463 | resolution: 464 | { integrity: sha512-QE2MgfOTem00qrlPgyByaCHay9yb1+9BjnMFnSFkUKQfu7adBXDTnCAivURnuPPAG/qiB+kzKkZKmKfaMT0zVg== } 465 | engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } 466 | peerDependencies: 467 | eslint: ^8.57.0 || ^9.0.0 468 | 469 | "@typescript-eslint/visitor-keys@8.8.0": 470 | resolution: 471 | { integrity: sha512-8mq51Lx6Hpmd7HnA2fcHQo3YgfX1qbccxQOgZcb4tvasu//zXRaA1j5ZRFeCw/VRAdFi4mRM9DnZw0Nu0Q2d1g== } 472 | engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } 473 | 474 | "@ungap/structured-clone@1.2.0": 475 | resolution: 476 | { integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== } 477 | 478 | acorn-jsx@5.3.2: 479 | resolution: 480 | { integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== } 481 | peerDependencies: 482 | acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 483 | 484 | acorn@8.12.1: 485 | resolution: 486 | { integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== } 487 | engines: { node: ">=0.4.0" } 488 | hasBin: true 489 | 490 | ajv@6.12.6: 491 | resolution: 492 | { integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== } 493 | 494 | ansi-regex@5.0.1: 495 | resolution: 496 | { integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== } 497 | engines: { node: ">=8" } 498 | 499 | ansi-regex@6.1.0: 500 | resolution: 501 | { integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== } 502 | engines: { node: ">=12" } 503 | 504 | ansi-styles@4.3.0: 505 | resolution: 506 | { integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== } 507 | engines: { node: ">=8" } 508 | 509 | ansi-styles@6.2.1: 510 | resolution: 511 | { integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== } 512 | engines: { node: ">=12" } 513 | 514 | argparse@2.0.1: 515 | resolution: 516 | { integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== } 517 | 518 | aria-query@5.1.3: 519 | resolution: 520 | { integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== } 521 | 522 | array-buffer-byte-length@1.0.1: 523 | resolution: 524 | { integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== } 525 | engines: { node: ">= 0.4" } 526 | 527 | array-includes@3.1.8: 528 | resolution: 529 | { integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== } 530 | engines: { node: ">= 0.4" } 531 | 532 | array.prototype.findlast@1.2.5: 533 | resolution: 534 | { integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== } 535 | engines: { node: ">= 0.4" } 536 | 537 | array.prototype.findlastindex@1.2.5: 538 | resolution: 539 | { integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ== } 540 | engines: { node: ">= 0.4" } 541 | 542 | array.prototype.flat@1.3.2: 543 | resolution: 544 | { integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== } 545 | engines: { node: ">= 0.4" } 546 | 547 | array.prototype.flatmap@1.3.2: 548 | resolution: 549 | { integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== } 550 | engines: { node: ">= 0.4" } 551 | 552 | array.prototype.tosorted@1.1.4: 553 | resolution: 554 | { integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA== } 555 | engines: { node: ">= 0.4" } 556 | 557 | arraybuffer.prototype.slice@1.0.3: 558 | resolution: 559 | { integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== } 560 | engines: { node: ">= 0.4" } 561 | 562 | ast-types-flow@0.0.8: 563 | resolution: 564 | { integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== } 565 | 566 | available-typed-arrays@1.0.7: 567 | resolution: 568 | { integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== } 569 | engines: { node: ">= 0.4" } 570 | 571 | axe-core@4.10.0: 572 | resolution: 573 | { integrity: sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g== } 574 | engines: { node: ">=4" } 575 | 576 | axobject-query@4.1.0: 577 | resolution: 578 | { integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ== } 579 | engines: { node: ">= 0.4" } 580 | 581 | balanced-match@1.0.2: 582 | resolution: 583 | { integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== } 584 | 585 | base64-js@1.5.1: 586 | resolution: 587 | { integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== } 588 | 589 | better-sqlite3@11.3.0: 590 | resolution: 591 | { integrity: sha512-iHt9j8NPYF3oKCNOO5ZI4JwThjt3Z6J6XrcwG85VNMVzv1ByqrHWv5VILEbCMFWDsoHhXvQ7oC8vgRXFAKgl9w== } 592 | 593 | bindings@1.5.0: 594 | resolution: 595 | { integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== } 596 | 597 | bl@4.1.0: 598 | resolution: 599 | { integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== } 600 | 601 | brace-expansion@1.1.11: 602 | resolution: 603 | { integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== } 604 | 605 | brace-expansion@2.0.1: 606 | resolution: 607 | { integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== } 608 | 609 | braces@3.0.3: 610 | resolution: 611 | { integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== } 612 | engines: { node: ">=8" } 613 | 614 | buffer@5.7.1: 615 | resolution: 616 | { integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== } 617 | 618 | busboy@1.6.0: 619 | resolution: 620 | { integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== } 621 | engines: { node: ">=10.16.0" } 622 | 623 | call-bind@1.0.7: 624 | resolution: 625 | { integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== } 626 | engines: { node: ">= 0.4" } 627 | 628 | callsites@3.1.0: 629 | resolution: 630 | { integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== } 631 | engines: { node: ">=6" } 632 | 633 | caniuse-lite@1.0.30001667: 634 | resolution: 635 | { integrity: sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw== } 636 | 637 | chalk@4.1.2: 638 | resolution: 639 | { integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== } 640 | engines: { node: ">=10" } 641 | 642 | chownr@1.1.4: 643 | resolution: 644 | { integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== } 645 | 646 | client-only@0.0.1: 647 | resolution: 648 | { integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== } 649 | 650 | color-convert@2.0.1: 651 | resolution: 652 | { integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== } 653 | engines: { node: ">=7.0.0" } 654 | 655 | color-name@1.1.4: 656 | resolution: 657 | { integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== } 658 | 659 | concat-map@0.0.1: 660 | resolution: 661 | { integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== } 662 | 663 | cookie-es@1.2.2: 664 | resolution: 665 | { integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg== } 666 | 667 | cross-spawn@7.0.3: 668 | resolution: 669 | { integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== } 670 | engines: { node: ">= 8" } 671 | 672 | csstype@3.1.3: 673 | resolution: 674 | { integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== } 675 | 676 | damerau-levenshtein@1.0.8: 677 | resolution: 678 | { integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== } 679 | 680 | data-view-buffer@1.0.1: 681 | resolution: 682 | { integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== } 683 | engines: { node: ">= 0.4" } 684 | 685 | data-view-byte-length@1.0.1: 686 | resolution: 687 | { integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== } 688 | engines: { node: ">= 0.4" } 689 | 690 | data-view-byte-offset@1.0.0: 691 | resolution: 692 | { integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== } 693 | engines: { node: ">= 0.4" } 694 | 695 | debug@3.2.7: 696 | resolution: 697 | { integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== } 698 | peerDependencies: 699 | supports-color: "*" 700 | peerDependenciesMeta: 701 | supports-color: 702 | optional: true 703 | 704 | debug@4.3.7: 705 | resolution: 706 | { integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== } 707 | engines: { node: ">=6.0" } 708 | peerDependencies: 709 | supports-color: "*" 710 | peerDependenciesMeta: 711 | supports-color: 712 | optional: true 713 | 714 | decompress-response@6.0.0: 715 | resolution: 716 | { integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== } 717 | engines: { node: ">=10" } 718 | 719 | deep-equal@2.2.3: 720 | resolution: 721 | { integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA== } 722 | engines: { node: ">= 0.4" } 723 | 724 | deep-extend@0.6.0: 725 | resolution: 726 | { integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== } 727 | engines: { node: ">=4.0.0" } 728 | 729 | deep-is@0.1.4: 730 | resolution: 731 | { integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== } 732 | 733 | define-data-property@1.1.4: 734 | resolution: 735 | { integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== } 736 | engines: { node: ">= 0.4" } 737 | 738 | define-properties@1.2.1: 739 | resolution: 740 | { integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== } 741 | engines: { node: ">= 0.4" } 742 | 743 | detect-libc@2.0.3: 744 | resolution: 745 | { integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== } 746 | engines: { node: ">=8" } 747 | 748 | doctrine@2.1.0: 749 | resolution: 750 | { integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== } 751 | engines: { node: ">=0.10.0" } 752 | 753 | doctrine@3.0.0: 754 | resolution: 755 | { integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== } 756 | engines: { node: ">=6.0.0" } 757 | 758 | eastasianwidth@0.2.0: 759 | resolution: 760 | { integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== } 761 | 762 | emoji-regex@8.0.0: 763 | resolution: 764 | { integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== } 765 | 766 | emoji-regex@9.2.2: 767 | resolution: 768 | { integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== } 769 | 770 | end-of-stream@1.4.4: 771 | resolution: 772 | { integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== } 773 | 774 | enhanced-resolve@5.17.1: 775 | resolution: 776 | { integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== } 777 | engines: { node: ">=10.13.0" } 778 | 779 | es-abstract@1.23.3: 780 | resolution: 781 | { integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== } 782 | engines: { node: ">= 0.4" } 783 | 784 | es-define-property@1.0.0: 785 | resolution: 786 | { integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== } 787 | engines: { node: ">= 0.4" } 788 | 789 | es-errors@1.3.0: 790 | resolution: 791 | { integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== } 792 | engines: { node: ">= 0.4" } 793 | 794 | es-get-iterator@1.1.3: 795 | resolution: 796 | { integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== } 797 | 798 | es-iterator-helpers@1.0.19: 799 | resolution: 800 | { integrity: sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw== } 801 | engines: { node: ">= 0.4" } 802 | 803 | es-object-atoms@1.0.0: 804 | resolution: 805 | { integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== } 806 | engines: { node: ">= 0.4" } 807 | 808 | es-set-tostringtag@2.0.3: 809 | resolution: 810 | { integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== } 811 | engines: { node: ">= 0.4" } 812 | 813 | es-shim-unscopables@1.0.2: 814 | resolution: 815 | { integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== } 816 | 817 | es-to-primitive@1.2.1: 818 | resolution: 819 | { integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== } 820 | engines: { node: ">= 0.4" } 821 | 822 | escape-string-regexp@4.0.0: 823 | resolution: 824 | { integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== } 825 | engines: { node: ">=10" } 826 | 827 | eslint-config-next@14.2.14: 828 | resolution: 829 | { integrity: sha512-TXwyjGICAlWC9O0OufS3koTsBKQH8l1xt3SY/aDuvtKHIwjTHplJKWVb1WOEX0OsDaxGbFXmfD2EY1sNfG0Y/w== } 830 | peerDependencies: 831 | eslint: ^7.23.0 || ^8.0.0 832 | typescript: ">=3.3.1" 833 | peerDependenciesMeta: 834 | typescript: 835 | optional: true 836 | 837 | eslint-config-prettier@9.1.0: 838 | resolution: 839 | { integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw== } 840 | hasBin: true 841 | peerDependencies: 842 | eslint: ">=7.0.0" 843 | 844 | eslint-import-resolver-node@0.3.9: 845 | resolution: 846 | { integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== } 847 | 848 | eslint-import-resolver-typescript@3.6.3: 849 | resolution: 850 | { integrity: sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA== } 851 | engines: { node: ^14.18.0 || >=16.0.0 } 852 | peerDependencies: 853 | eslint: "*" 854 | eslint-plugin-import: "*" 855 | eslint-plugin-import-x: "*" 856 | peerDependenciesMeta: 857 | eslint-plugin-import: 858 | optional: true 859 | eslint-plugin-import-x: 860 | optional: true 861 | 862 | eslint-module-utils@2.12.0: 863 | resolution: 864 | { integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg== } 865 | engines: { node: ">=4" } 866 | peerDependencies: 867 | "@typescript-eslint/parser": "*" 868 | eslint: "*" 869 | eslint-import-resolver-node: "*" 870 | eslint-import-resolver-typescript: "*" 871 | eslint-import-resolver-webpack: "*" 872 | peerDependenciesMeta: 873 | "@typescript-eslint/parser": 874 | optional: true 875 | eslint: 876 | optional: true 877 | eslint-import-resolver-node: 878 | optional: true 879 | eslint-import-resolver-typescript: 880 | optional: true 881 | eslint-import-resolver-webpack: 882 | optional: true 883 | 884 | eslint-plugin-import@2.31.0: 885 | resolution: 886 | { integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A== } 887 | engines: { node: ">=4" } 888 | peerDependencies: 889 | "@typescript-eslint/parser": "*" 890 | eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 891 | peerDependenciesMeta: 892 | "@typescript-eslint/parser": 893 | optional: true 894 | 895 | eslint-plugin-jsx-a11y@6.10.0: 896 | resolution: 897 | { integrity: sha512-ySOHvXX8eSN6zz8Bywacm7CvGNhUtdjvqfQDVe6020TUK34Cywkw7m0KsCCk1Qtm9G1FayfTN1/7mMYnYO2Bhg== } 898 | engines: { node: ">=4.0" } 899 | peerDependencies: 900 | eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 901 | 902 | eslint-plugin-react-hooks@4.6.2: 903 | resolution: 904 | { integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ== } 905 | engines: { node: ">=10" } 906 | peerDependencies: 907 | eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 908 | 909 | eslint-plugin-react@7.37.1: 910 | resolution: 911 | { integrity: sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg== } 912 | engines: { node: ">=4" } 913 | peerDependencies: 914 | eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 915 | 916 | eslint-scope@7.2.2: 917 | resolution: 918 | { integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== } 919 | engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } 920 | 921 | eslint-visitor-keys@3.4.3: 922 | resolution: 923 | { integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== } 924 | engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } 925 | 926 | eslint@8.57.1: 927 | resolution: 928 | { integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== } 929 | engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } 930 | hasBin: true 931 | 932 | espree@9.6.1: 933 | resolution: 934 | { integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== } 935 | engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } 936 | 937 | esquery@1.6.0: 938 | resolution: 939 | { integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== } 940 | engines: { node: ">=0.10" } 941 | 942 | esrecurse@4.3.0: 943 | resolution: 944 | { integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== } 945 | engines: { node: ">=4.0" } 946 | 947 | estraverse@5.3.0: 948 | resolution: 949 | { integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== } 950 | engines: { node: ">=4.0" } 951 | 952 | esutils@2.0.3: 953 | resolution: 954 | { integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== } 955 | engines: { node: ">=0.10.0" } 956 | 957 | expand-template@2.0.3: 958 | resolution: 959 | { integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== } 960 | engines: { node: ">=6" } 961 | 962 | fast-deep-equal@3.1.3: 963 | resolution: 964 | { integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== } 965 | 966 | fast-glob@3.3.2: 967 | resolution: 968 | { integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== } 969 | engines: { node: ">=8.6.0" } 970 | 971 | fast-json-stable-stringify@2.1.0: 972 | resolution: 973 | { integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== } 974 | 975 | fast-levenshtein@2.0.6: 976 | resolution: 977 | { integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== } 978 | 979 | fastq@1.17.1: 980 | resolution: 981 | { integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== } 982 | 983 | file-entry-cache@6.0.1: 984 | resolution: 985 | { integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== } 986 | engines: { node: ^10.12.0 || >=12.0.0 } 987 | 988 | file-uri-to-path@1.0.0: 989 | resolution: 990 | { integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== } 991 | 992 | fill-range@7.1.1: 993 | resolution: 994 | { integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== } 995 | engines: { node: ">=8" } 996 | 997 | find-up@5.0.0: 998 | resolution: 999 | { integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== } 1000 | engines: { node: ">=10" } 1001 | 1002 | flat-cache@3.2.0: 1003 | resolution: 1004 | { integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== } 1005 | engines: { node: ^10.12.0 || >=12.0.0 } 1006 | 1007 | flatted@3.3.1: 1008 | resolution: 1009 | { integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== } 1010 | 1011 | for-each@0.3.3: 1012 | resolution: 1013 | { integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== } 1014 | 1015 | foreground-child@3.3.0: 1016 | resolution: 1017 | { integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== } 1018 | engines: { node: ">=14" } 1019 | 1020 | fs-constants@1.0.0: 1021 | resolution: 1022 | { integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== } 1023 | 1024 | fs.realpath@1.0.0: 1025 | resolution: 1026 | { integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== } 1027 | 1028 | function-bind@1.1.2: 1029 | resolution: 1030 | { integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== } 1031 | 1032 | function.prototype.name@1.1.6: 1033 | resolution: 1034 | { integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== } 1035 | engines: { node: ">= 0.4" } 1036 | 1037 | functions-have-names@1.2.3: 1038 | resolution: 1039 | { integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== } 1040 | 1041 | get-intrinsic@1.2.4: 1042 | resolution: 1043 | { integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== } 1044 | engines: { node: ">= 0.4" } 1045 | 1046 | get-symbol-description@1.0.2: 1047 | resolution: 1048 | { integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== } 1049 | engines: { node: ">= 0.4" } 1050 | 1051 | get-tsconfig@4.8.1: 1052 | resolution: 1053 | { integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg== } 1054 | 1055 | github-from-package@0.0.0: 1056 | resolution: 1057 | { integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== } 1058 | 1059 | glob-parent@5.1.2: 1060 | resolution: 1061 | { integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== } 1062 | engines: { node: ">= 6" } 1063 | 1064 | glob-parent@6.0.2: 1065 | resolution: 1066 | { integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== } 1067 | engines: { node: ">=10.13.0" } 1068 | 1069 | glob@10.3.10: 1070 | resolution: 1071 | { integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== } 1072 | engines: { node: ">=16 || 14 >=14.17" } 1073 | hasBin: true 1074 | 1075 | glob@7.2.3: 1076 | resolution: 1077 | { integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== } 1078 | deprecated: Glob versions prior to v9 are no longer supported 1079 | 1080 | globals@13.24.0: 1081 | resolution: 1082 | { integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== } 1083 | engines: { node: ">=8" } 1084 | 1085 | globalthis@1.0.4: 1086 | resolution: 1087 | { integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== } 1088 | engines: { node: ">= 0.4" } 1089 | 1090 | gopd@1.0.1: 1091 | resolution: 1092 | { integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== } 1093 | 1094 | graceful-fs@4.2.11: 1095 | resolution: 1096 | { integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== } 1097 | 1098 | graphemer@1.4.0: 1099 | resolution: 1100 | { integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== } 1101 | 1102 | has-bigints@1.0.2: 1103 | resolution: 1104 | { integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== } 1105 | 1106 | has-flag@4.0.0: 1107 | resolution: 1108 | { integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== } 1109 | engines: { node: ">=8" } 1110 | 1111 | has-property-descriptors@1.0.2: 1112 | resolution: 1113 | { integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== } 1114 | 1115 | has-proto@1.0.3: 1116 | resolution: 1117 | { integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== } 1118 | engines: { node: ">= 0.4" } 1119 | 1120 | has-symbols@1.0.3: 1121 | resolution: 1122 | { integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== } 1123 | engines: { node: ">= 0.4" } 1124 | 1125 | has-tostringtag@1.0.2: 1126 | resolution: 1127 | { integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== } 1128 | engines: { node: ">= 0.4" } 1129 | 1130 | hasown@2.0.2: 1131 | resolution: 1132 | { integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== } 1133 | engines: { node: ">= 0.4" } 1134 | 1135 | ieee754@1.2.1: 1136 | resolution: 1137 | { integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== } 1138 | 1139 | ignore@5.3.2: 1140 | resolution: 1141 | { integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== } 1142 | engines: { node: ">= 4" } 1143 | 1144 | import-fresh@3.3.0: 1145 | resolution: 1146 | { integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== } 1147 | engines: { node: ">=6" } 1148 | 1149 | imurmurhash@0.1.4: 1150 | resolution: 1151 | { integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== } 1152 | engines: { node: ">=0.8.19" } 1153 | 1154 | inflight@1.0.6: 1155 | resolution: 1156 | { integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== } 1157 | deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. 1158 | 1159 | inherits@2.0.4: 1160 | resolution: 1161 | { integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== } 1162 | 1163 | ini@1.3.8: 1164 | resolution: 1165 | { integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== } 1166 | 1167 | internal-slot@1.0.7: 1168 | resolution: 1169 | { integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== } 1170 | engines: { node: ">= 0.4" } 1171 | 1172 | is-arguments@1.1.1: 1173 | resolution: 1174 | { integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== } 1175 | engines: { node: ">= 0.4" } 1176 | 1177 | is-array-buffer@3.0.4: 1178 | resolution: 1179 | { integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== } 1180 | engines: { node: ">= 0.4" } 1181 | 1182 | is-async-function@2.0.0: 1183 | resolution: 1184 | { integrity: sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA== } 1185 | engines: { node: ">= 0.4" } 1186 | 1187 | is-bigint@1.0.4: 1188 | resolution: 1189 | { integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== } 1190 | 1191 | is-boolean-object@1.1.2: 1192 | resolution: 1193 | { integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== } 1194 | engines: { node: ">= 0.4" } 1195 | 1196 | is-bun-module@1.2.1: 1197 | resolution: 1198 | { integrity: sha512-AmidtEM6D6NmUiLOvvU7+IePxjEjOzra2h0pSrsfSAcXwl/83zLLXDByafUJy9k/rKK0pvXMLdwKwGHlX2Ke6Q== } 1199 | 1200 | is-callable@1.2.7: 1201 | resolution: 1202 | { integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== } 1203 | engines: { node: ">= 0.4" } 1204 | 1205 | is-core-module@2.15.1: 1206 | resolution: 1207 | { integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== } 1208 | engines: { node: ">= 0.4" } 1209 | 1210 | is-data-view@1.0.1: 1211 | resolution: 1212 | { integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== } 1213 | engines: { node: ">= 0.4" } 1214 | 1215 | is-date-object@1.0.5: 1216 | resolution: 1217 | { integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== } 1218 | engines: { node: ">= 0.4" } 1219 | 1220 | is-extglob@2.1.1: 1221 | resolution: 1222 | { integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== } 1223 | engines: { node: ">=0.10.0" } 1224 | 1225 | is-finalizationregistry@1.0.2: 1226 | resolution: 1227 | { integrity: sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw== } 1228 | 1229 | is-fullwidth-code-point@3.0.0: 1230 | resolution: 1231 | { integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== } 1232 | engines: { node: ">=8" } 1233 | 1234 | is-generator-function@1.0.10: 1235 | resolution: 1236 | { integrity: sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== } 1237 | engines: { node: ">= 0.4" } 1238 | 1239 | is-glob@4.0.3: 1240 | resolution: 1241 | { integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== } 1242 | engines: { node: ">=0.10.0" } 1243 | 1244 | is-map@2.0.3: 1245 | resolution: 1246 | { integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== } 1247 | engines: { node: ">= 0.4" } 1248 | 1249 | is-negative-zero@2.0.3: 1250 | resolution: 1251 | { integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== } 1252 | engines: { node: ">= 0.4" } 1253 | 1254 | is-number-object@1.0.7: 1255 | resolution: 1256 | { integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== } 1257 | engines: { node: ">= 0.4" } 1258 | 1259 | is-number@7.0.0: 1260 | resolution: 1261 | { integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== } 1262 | engines: { node: ">=0.12.0" } 1263 | 1264 | is-path-inside@3.0.3: 1265 | resolution: 1266 | { integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== } 1267 | engines: { node: ">=8" } 1268 | 1269 | is-regex@1.1.4: 1270 | resolution: 1271 | { integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== } 1272 | engines: { node: ">= 0.4" } 1273 | 1274 | is-set@2.0.3: 1275 | resolution: 1276 | { integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== } 1277 | engines: { node: ">= 0.4" } 1278 | 1279 | is-shared-array-buffer@1.0.3: 1280 | resolution: 1281 | { integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== } 1282 | engines: { node: ">= 0.4" } 1283 | 1284 | is-string@1.0.7: 1285 | resolution: 1286 | { integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== } 1287 | engines: { node: ">= 0.4" } 1288 | 1289 | is-symbol@1.0.4: 1290 | resolution: 1291 | { integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== } 1292 | engines: { node: ">= 0.4" } 1293 | 1294 | is-typed-array@1.1.13: 1295 | resolution: 1296 | { integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== } 1297 | engines: { node: ">= 0.4" } 1298 | 1299 | is-weakmap@2.0.2: 1300 | resolution: 1301 | { integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== } 1302 | engines: { node: ">= 0.4" } 1303 | 1304 | is-weakref@1.0.2: 1305 | resolution: 1306 | { integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== } 1307 | 1308 | is-weakset@2.0.3: 1309 | resolution: 1310 | { integrity: sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ== } 1311 | engines: { node: ">= 0.4" } 1312 | 1313 | isarray@2.0.5: 1314 | resolution: 1315 | { integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== } 1316 | 1317 | isexe@2.0.0: 1318 | resolution: 1319 | { integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== } 1320 | 1321 | iterator.prototype@1.1.2: 1322 | resolution: 1323 | { integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w== } 1324 | 1325 | jackspeak@2.3.6: 1326 | resolution: 1327 | { integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== } 1328 | engines: { node: ">=14" } 1329 | 1330 | js-tokens@4.0.0: 1331 | resolution: 1332 | { integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== } 1333 | 1334 | js-yaml@4.1.0: 1335 | resolution: 1336 | { integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== } 1337 | hasBin: true 1338 | 1339 | json-buffer@3.0.1: 1340 | resolution: 1341 | { integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== } 1342 | 1343 | json-schema-traverse@0.4.1: 1344 | resolution: 1345 | { integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== } 1346 | 1347 | json-stable-stringify-without-jsonify@1.0.1: 1348 | resolution: 1349 | { integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== } 1350 | 1351 | json5@1.0.2: 1352 | resolution: 1353 | { integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== } 1354 | hasBin: true 1355 | 1356 | jsx-ast-utils@3.3.5: 1357 | resolution: 1358 | { integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== } 1359 | engines: { node: ">=4.0" } 1360 | 1361 | keyv@4.5.4: 1362 | resolution: 1363 | { integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== } 1364 | 1365 | language-subtag-registry@0.3.23: 1366 | resolution: 1367 | { integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ== } 1368 | 1369 | language-tags@1.0.9: 1370 | resolution: 1371 | { integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA== } 1372 | engines: { node: ">=0.10" } 1373 | 1374 | levn@0.4.1: 1375 | resolution: 1376 | { integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== } 1377 | engines: { node: ">= 0.8.0" } 1378 | 1379 | locate-path@6.0.0: 1380 | resolution: 1381 | { integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== } 1382 | engines: { node: ">=10" } 1383 | 1384 | lodash.merge@4.6.2: 1385 | resolution: 1386 | { integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== } 1387 | 1388 | loose-envify@1.4.0: 1389 | resolution: 1390 | { integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== } 1391 | hasBin: true 1392 | 1393 | lru-cache@10.4.3: 1394 | resolution: 1395 | { integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== } 1396 | 1397 | merge2@1.4.1: 1398 | resolution: 1399 | { integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== } 1400 | engines: { node: ">= 8" } 1401 | 1402 | micromatch@4.0.8: 1403 | resolution: 1404 | { integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== } 1405 | engines: { node: ">=8.6" } 1406 | 1407 | mimic-response@3.1.0: 1408 | resolution: 1409 | { integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== } 1410 | engines: { node: ">=10" } 1411 | 1412 | minimatch@3.1.2: 1413 | resolution: 1414 | { integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== } 1415 | 1416 | minimatch@9.0.5: 1417 | resolution: 1418 | { integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== } 1419 | engines: { node: ">=16 || 14 >=14.17" } 1420 | 1421 | minimist@1.2.8: 1422 | resolution: 1423 | { integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== } 1424 | 1425 | minipass@7.1.2: 1426 | resolution: 1427 | { integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== } 1428 | engines: { node: ">=16 || 14 >=14.17" } 1429 | 1430 | mkdirp-classic@0.5.3: 1431 | resolution: 1432 | { integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== } 1433 | 1434 | ms@2.1.3: 1435 | resolution: 1436 | { integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== } 1437 | 1438 | nanoid@3.3.7: 1439 | resolution: 1440 | { integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== } 1441 | engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } 1442 | hasBin: true 1443 | 1444 | napi-build-utils@1.0.2: 1445 | resolution: 1446 | { integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg== } 1447 | 1448 | natural-compare@1.4.0: 1449 | resolution: 1450 | { integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== } 1451 | 1452 | next@14.2.14: 1453 | resolution: 1454 | { integrity: sha512-Q1coZG17MW0Ly5x76shJ4dkC23woLAhhnDnw+DfTc7EpZSGuWrlsZ3bZaO8t6u1Yu8FVfhkqJE+U8GC7E0GLPQ== } 1455 | engines: { node: ">=18.17.0" } 1456 | hasBin: true 1457 | peerDependencies: 1458 | "@opentelemetry/api": ^1.1.0 1459 | "@playwright/test": ^1.41.2 1460 | react: ^18.2.0 1461 | react-dom: ^18.2.0 1462 | sass: ^1.3.0 1463 | peerDependenciesMeta: 1464 | "@opentelemetry/api": 1465 | optional: true 1466 | "@playwright/test": 1467 | optional: true 1468 | sass: 1469 | optional: true 1470 | 1471 | node-abi@3.68.0: 1472 | resolution: 1473 | { integrity: sha512-7vbj10trelExNjFSBm5kTvZXXa7pZyKWx9RCKIyqe6I9Ev3IzGpQoqBP3a+cOdxY+pWj6VkP28n/2wWysBHD/A== } 1474 | engines: { node: ">=10" } 1475 | 1476 | object-assign@4.1.1: 1477 | resolution: 1478 | { integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== } 1479 | engines: { node: ">=0.10.0" } 1480 | 1481 | object-inspect@1.13.2: 1482 | resolution: 1483 | { integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== } 1484 | engines: { node: ">= 0.4" } 1485 | 1486 | object-is@1.1.6: 1487 | resolution: 1488 | { integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== } 1489 | engines: { node: ">= 0.4" } 1490 | 1491 | object-keys@1.1.1: 1492 | resolution: 1493 | { integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== } 1494 | engines: { node: ">= 0.4" } 1495 | 1496 | object.assign@4.1.5: 1497 | resolution: 1498 | { integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== } 1499 | engines: { node: ">= 0.4" } 1500 | 1501 | object.entries@1.1.8: 1502 | resolution: 1503 | { integrity: sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== } 1504 | engines: { node: ">= 0.4" } 1505 | 1506 | object.fromentries@2.0.8: 1507 | resolution: 1508 | { integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== } 1509 | engines: { node: ">= 0.4" } 1510 | 1511 | object.groupby@1.0.3: 1512 | resolution: 1513 | { integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== } 1514 | engines: { node: ">= 0.4" } 1515 | 1516 | object.values@1.2.0: 1517 | resolution: 1518 | { integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== } 1519 | engines: { node: ">= 0.4" } 1520 | 1521 | once@1.4.0: 1522 | resolution: 1523 | { integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== } 1524 | 1525 | optionator@0.9.4: 1526 | resolution: 1527 | { integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== } 1528 | engines: { node: ">= 0.8.0" } 1529 | 1530 | p-limit@3.1.0: 1531 | resolution: 1532 | { integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== } 1533 | engines: { node: ">=10" } 1534 | 1535 | p-locate@5.0.0: 1536 | resolution: 1537 | { integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== } 1538 | engines: { node: ">=10" } 1539 | 1540 | parent-module@1.0.1: 1541 | resolution: 1542 | { integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== } 1543 | engines: { node: ">=6" } 1544 | 1545 | path-exists@4.0.0: 1546 | resolution: 1547 | { integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== } 1548 | engines: { node: ">=8" } 1549 | 1550 | path-is-absolute@1.0.1: 1551 | resolution: 1552 | { integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== } 1553 | engines: { node: ">=0.10.0" } 1554 | 1555 | path-key@3.1.1: 1556 | resolution: 1557 | { integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== } 1558 | engines: { node: ">=8" } 1559 | 1560 | path-parse@1.0.7: 1561 | resolution: 1562 | { integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== } 1563 | 1564 | path-scurry@1.11.1: 1565 | resolution: 1566 | { integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== } 1567 | engines: { node: ">=16 || 14 >=14.18" } 1568 | 1569 | picocolors@1.1.0: 1570 | resolution: 1571 | { integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== } 1572 | 1573 | picomatch@2.3.1: 1574 | resolution: 1575 | { integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== } 1576 | engines: { node: ">=8.6" } 1577 | 1578 | possible-typed-array-names@1.0.0: 1579 | resolution: 1580 | { integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== } 1581 | engines: { node: ">= 0.4" } 1582 | 1583 | postcss@8.4.31: 1584 | resolution: 1585 | { integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== } 1586 | engines: { node: ^10 || ^12 || >=14 } 1587 | 1588 | prebuild-install@7.1.2: 1589 | resolution: 1590 | { integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ== } 1591 | engines: { node: ">=10" } 1592 | hasBin: true 1593 | 1594 | prelude-ls@1.2.1: 1595 | resolution: 1596 | { integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== } 1597 | engines: { node: ">= 0.8.0" } 1598 | 1599 | prettier@3.3.3: 1600 | resolution: 1601 | { integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== } 1602 | engines: { node: ">=14" } 1603 | hasBin: true 1604 | 1605 | prop-types@15.8.1: 1606 | resolution: 1607 | { integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== } 1608 | 1609 | pump@3.0.2: 1610 | resolution: 1611 | { integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw== } 1612 | 1613 | punycode@2.3.1: 1614 | resolution: 1615 | { integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== } 1616 | engines: { node: ">=6" } 1617 | 1618 | queue-microtask@1.2.3: 1619 | resolution: 1620 | { integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== } 1621 | 1622 | rc@1.2.8: 1623 | resolution: 1624 | { integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== } 1625 | hasBin: true 1626 | 1627 | react-dom@18.3.1: 1628 | resolution: 1629 | { integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== } 1630 | peerDependencies: 1631 | react: ^18.3.1 1632 | 1633 | react-is@16.13.1: 1634 | resolution: 1635 | { integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== } 1636 | 1637 | react@18.3.1: 1638 | resolution: 1639 | { integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== } 1640 | engines: { node: ">=0.10.0" } 1641 | 1642 | readable-stream@3.6.2: 1643 | resolution: 1644 | { integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== } 1645 | engines: { node: ">= 6" } 1646 | 1647 | reflect.getprototypeof@1.0.6: 1648 | resolution: 1649 | { integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg== } 1650 | engines: { node: ">= 0.4" } 1651 | 1652 | regexp.prototype.flags@1.5.3: 1653 | resolution: 1654 | { integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ== } 1655 | engines: { node: ">= 0.4" } 1656 | 1657 | resolve-from@4.0.0: 1658 | resolution: 1659 | { integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== } 1660 | engines: { node: ">=4" } 1661 | 1662 | resolve-pkg-maps@1.0.0: 1663 | resolution: 1664 | { integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== } 1665 | 1666 | resolve@1.22.8: 1667 | resolution: 1668 | { integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== } 1669 | hasBin: true 1670 | 1671 | resolve@2.0.0-next.5: 1672 | resolution: 1673 | { integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== } 1674 | hasBin: true 1675 | 1676 | reusify@1.0.4: 1677 | resolution: 1678 | { integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== } 1679 | engines: { iojs: ">=1.0.0", node: ">=0.10.0" } 1680 | 1681 | rimraf@3.0.2: 1682 | resolution: 1683 | { integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== } 1684 | deprecated: Rimraf versions prior to v4 are no longer supported 1685 | hasBin: true 1686 | 1687 | run-parallel@1.2.0: 1688 | resolution: 1689 | { integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== } 1690 | 1691 | safe-array-concat@1.1.2: 1692 | resolution: 1693 | { integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== } 1694 | engines: { node: ">=0.4" } 1695 | 1696 | safe-buffer@5.2.1: 1697 | resolution: 1698 | { integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== } 1699 | 1700 | safe-regex-test@1.0.3: 1701 | resolution: 1702 | { integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== } 1703 | engines: { node: ">= 0.4" } 1704 | 1705 | scheduler@0.23.2: 1706 | resolution: 1707 | { integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== } 1708 | 1709 | semver@6.3.1: 1710 | resolution: 1711 | { integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== } 1712 | hasBin: true 1713 | 1714 | semver@7.6.3: 1715 | resolution: 1716 | { integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== } 1717 | engines: { node: ">=10" } 1718 | hasBin: true 1719 | 1720 | set-function-length@1.2.2: 1721 | resolution: 1722 | { integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== } 1723 | engines: { node: ">= 0.4" } 1724 | 1725 | set-function-name@2.0.2: 1726 | resolution: 1727 | { integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== } 1728 | engines: { node: ">= 0.4" } 1729 | 1730 | shebang-command@2.0.0: 1731 | resolution: 1732 | { integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== } 1733 | engines: { node: ">=8" } 1734 | 1735 | shebang-regex@3.0.0: 1736 | resolution: 1737 | { integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== } 1738 | engines: { node: ">=8" } 1739 | 1740 | side-channel@1.0.6: 1741 | resolution: 1742 | { integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== } 1743 | engines: { node: ">= 0.4" } 1744 | 1745 | signal-exit@4.1.0: 1746 | resolution: 1747 | { integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== } 1748 | engines: { node: ">=14" } 1749 | 1750 | simple-concat@1.0.1: 1751 | resolution: 1752 | { integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q== } 1753 | 1754 | simple-get@4.0.1: 1755 | resolution: 1756 | { integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== } 1757 | 1758 | source-map-js@1.2.1: 1759 | resolution: 1760 | { integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== } 1761 | engines: { node: ">=0.10.0" } 1762 | 1763 | stop-iteration-iterator@1.0.0: 1764 | resolution: 1765 | { integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== } 1766 | engines: { node: ">= 0.4" } 1767 | 1768 | streamsearch@1.1.0: 1769 | resolution: 1770 | { integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== } 1771 | engines: { node: ">=10.0.0" } 1772 | 1773 | string-width@4.2.3: 1774 | resolution: 1775 | { integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== } 1776 | engines: { node: ">=8" } 1777 | 1778 | string-width@5.1.2: 1779 | resolution: 1780 | { integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== } 1781 | engines: { node: ">=12" } 1782 | 1783 | string.prototype.includes@2.0.0: 1784 | resolution: 1785 | { integrity: sha512-E34CkBgyeqNDcrbU76cDjL5JLcVrtSdYq0MEh/B10r17pRP4ciHLwTgnuLV8Ay6cgEMLkcBkFCKyFZ43YldYzg== } 1786 | 1787 | string.prototype.matchall@4.0.11: 1788 | resolution: 1789 | { integrity: sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg== } 1790 | engines: { node: ">= 0.4" } 1791 | 1792 | string.prototype.repeat@1.0.0: 1793 | resolution: 1794 | { integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w== } 1795 | 1796 | string.prototype.trim@1.2.9: 1797 | resolution: 1798 | { integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== } 1799 | engines: { node: ">= 0.4" } 1800 | 1801 | string.prototype.trimend@1.0.8: 1802 | resolution: 1803 | { integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== } 1804 | 1805 | string.prototype.trimstart@1.0.8: 1806 | resolution: 1807 | { integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== } 1808 | engines: { node: ">= 0.4" } 1809 | 1810 | string_decoder@1.3.0: 1811 | resolution: 1812 | { integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== } 1813 | 1814 | strip-ansi@6.0.1: 1815 | resolution: 1816 | { integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== } 1817 | engines: { node: ">=8" } 1818 | 1819 | strip-ansi@7.1.0: 1820 | resolution: 1821 | { integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== } 1822 | engines: { node: ">=12" } 1823 | 1824 | strip-bom@3.0.0: 1825 | resolution: 1826 | { integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== } 1827 | engines: { node: ">=4" } 1828 | 1829 | strip-json-comments@2.0.1: 1830 | resolution: 1831 | { integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== } 1832 | engines: { node: ">=0.10.0" } 1833 | 1834 | strip-json-comments@3.1.1: 1835 | resolution: 1836 | { integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== } 1837 | engines: { node: ">=8" } 1838 | 1839 | styled-jsx@5.1.1: 1840 | resolution: 1841 | { integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw== } 1842 | engines: { node: ">= 12.0.0" } 1843 | peerDependencies: 1844 | "@babel/core": "*" 1845 | babel-plugin-macros: "*" 1846 | react: ">= 16.8.0 || 17.x.x || ^18.0.0-0" 1847 | peerDependenciesMeta: 1848 | "@babel/core": 1849 | optional: true 1850 | babel-plugin-macros: 1851 | optional: true 1852 | 1853 | supports-color@7.2.0: 1854 | resolution: 1855 | { integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== } 1856 | engines: { node: ">=8" } 1857 | 1858 | supports-preserve-symlinks-flag@1.0.0: 1859 | resolution: 1860 | { integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== } 1861 | engines: { node: ">= 0.4" } 1862 | 1863 | tapable@2.2.1: 1864 | resolution: 1865 | { integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== } 1866 | engines: { node: ">=6" } 1867 | 1868 | tar-fs@2.1.1: 1869 | resolution: 1870 | { integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== } 1871 | 1872 | tar-stream@2.2.0: 1873 | resolution: 1874 | { integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== } 1875 | engines: { node: ">=6" } 1876 | 1877 | text-table@0.2.0: 1878 | resolution: 1879 | { integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== } 1880 | 1881 | to-regex-range@5.0.1: 1882 | resolution: 1883 | { integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== } 1884 | engines: { node: ">=8.0" } 1885 | 1886 | ts-api-utils@1.3.0: 1887 | resolution: 1888 | { integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== } 1889 | engines: { node: ">=16" } 1890 | peerDependencies: 1891 | typescript: ">=4.2.0" 1892 | 1893 | tsconfig-paths@3.15.0: 1894 | resolution: 1895 | { integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== } 1896 | 1897 | tslib@2.7.0: 1898 | resolution: 1899 | { integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== } 1900 | 1901 | tunnel-agent@0.6.0: 1902 | resolution: 1903 | { integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== } 1904 | 1905 | type-check@0.4.0: 1906 | resolution: 1907 | { integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== } 1908 | engines: { node: ">= 0.8.0" } 1909 | 1910 | type-fest@0.20.2: 1911 | resolution: 1912 | { integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== } 1913 | engines: { node: ">=10" } 1914 | 1915 | typed-array-buffer@1.0.2: 1916 | resolution: 1917 | { integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== } 1918 | engines: { node: ">= 0.4" } 1919 | 1920 | typed-array-byte-length@1.0.1: 1921 | resolution: 1922 | { integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== } 1923 | engines: { node: ">= 0.4" } 1924 | 1925 | typed-array-byte-offset@1.0.2: 1926 | resolution: 1927 | { integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== } 1928 | engines: { node: ">= 0.4" } 1929 | 1930 | typed-array-length@1.0.6: 1931 | resolution: 1932 | { integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== } 1933 | engines: { node: ">= 0.4" } 1934 | 1935 | typescript@5.6.2: 1936 | resolution: 1937 | { integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw== } 1938 | engines: { node: ">=14.17" } 1939 | hasBin: true 1940 | 1941 | unbox-primitive@1.0.2: 1942 | resolution: 1943 | { integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== } 1944 | 1945 | undici-types@6.19.8: 1946 | resolution: 1947 | { integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== } 1948 | 1949 | uqr@0.1.2: 1950 | resolution: 1951 | { integrity: sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA== } 1952 | 1953 | uri-js@4.4.1: 1954 | resolution: 1955 | { integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== } 1956 | 1957 | util-deprecate@1.0.2: 1958 | resolution: 1959 | { integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== } 1960 | 1961 | which-boxed-primitive@1.0.2: 1962 | resolution: 1963 | { integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== } 1964 | 1965 | which-builtin-type@1.1.4: 1966 | resolution: 1967 | { integrity: sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w== } 1968 | engines: { node: ">= 0.4" } 1969 | 1970 | which-collection@1.0.2: 1971 | resolution: 1972 | { integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== } 1973 | engines: { node: ">= 0.4" } 1974 | 1975 | which-typed-array@1.1.15: 1976 | resolution: 1977 | { integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== } 1978 | engines: { node: ">= 0.4" } 1979 | 1980 | which@2.0.2: 1981 | resolution: 1982 | { integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== } 1983 | engines: { node: ">= 8" } 1984 | hasBin: true 1985 | 1986 | word-wrap@1.2.5: 1987 | resolution: 1988 | { integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== } 1989 | engines: { node: ">=0.10.0" } 1990 | 1991 | wrap-ansi@7.0.0: 1992 | resolution: 1993 | { integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== } 1994 | engines: { node: ">=10" } 1995 | 1996 | wrap-ansi@8.1.0: 1997 | resolution: 1998 | { integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== } 1999 | engines: { node: ">=12" } 2000 | 2001 | wrappy@1.0.2: 2002 | resolution: 2003 | { integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== } 2004 | 2005 | yocto-queue@0.1.0: 2006 | resolution: 2007 | { integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== } 2008 | engines: { node: ">=10" } 2009 | 2010 | snapshots: 2011 | "@emnapi/core@1.3.0": 2012 | dependencies: 2013 | "@emnapi/wasi-threads": 1.0.1 2014 | tslib: 2.7.0 2015 | optional: true 2016 | 2017 | "@emnapi/runtime@1.3.0": 2018 | dependencies: 2019 | tslib: 2.7.0 2020 | optional: true 2021 | 2022 | "@emnapi/wasi-threads@1.0.1": 2023 | dependencies: 2024 | tslib: 2.7.0 2025 | optional: true 2026 | 2027 | "@eslint-community/eslint-utils@4.4.0(eslint@8.57.1)": 2028 | dependencies: 2029 | eslint: 8.57.1 2030 | eslint-visitor-keys: 3.4.3 2031 | 2032 | "@eslint-community/regexpp@4.11.1": {} 2033 | 2034 | "@eslint/eslintrc@2.1.4": 2035 | dependencies: 2036 | ajv: 6.12.6 2037 | debug: 4.3.7 2038 | espree: 9.6.1 2039 | globals: 13.24.0 2040 | ignore: 5.3.2 2041 | import-fresh: 3.3.0 2042 | js-yaml: 4.1.0 2043 | minimatch: 3.1.2 2044 | strip-json-comments: 3.1.1 2045 | transitivePeerDependencies: 2046 | - supports-color 2047 | 2048 | "@eslint/js@8.57.1": {} 2049 | 2050 | "@humanwhocodes/config-array@0.13.0": 2051 | dependencies: 2052 | "@humanwhocodes/object-schema": 2.0.3 2053 | debug: 4.3.7 2054 | minimatch: 3.1.2 2055 | transitivePeerDependencies: 2056 | - supports-color 2057 | 2058 | "@humanwhocodes/module-importer@1.0.1": {} 2059 | 2060 | "@humanwhocodes/object-schema@2.0.3": {} 2061 | 2062 | "@isaacs/cliui@8.0.2": 2063 | dependencies: 2064 | string-width: 5.1.2 2065 | string-width-cjs: string-width@4.2.3 2066 | strip-ansi: 7.1.0 2067 | strip-ansi-cjs: strip-ansi@6.0.1 2068 | wrap-ansi: 8.1.0 2069 | wrap-ansi-cjs: wrap-ansi@7.0.0 2070 | 2071 | "@napi-rs/wasm-runtime@0.2.5": 2072 | dependencies: 2073 | "@emnapi/core": 1.3.0 2074 | "@emnapi/runtime": 1.3.0 2075 | "@tybys/wasm-util": 0.9.0 2076 | optional: true 2077 | 2078 | "@next/env@14.2.14": {} 2079 | 2080 | "@next/eslint-plugin-next@14.2.14": 2081 | dependencies: 2082 | glob: 10.3.10 2083 | 2084 | "@next/swc-darwin-arm64@14.2.14": 2085 | optional: true 2086 | 2087 | "@next/swc-darwin-x64@14.2.14": 2088 | optional: true 2089 | 2090 | "@next/swc-linux-arm64-gnu@14.2.14": 2091 | optional: true 2092 | 2093 | "@next/swc-linux-arm64-musl@14.2.14": 2094 | optional: true 2095 | 2096 | "@next/swc-linux-x64-gnu@14.2.14": 2097 | optional: true 2098 | 2099 | "@next/swc-linux-x64-musl@14.2.14": 2100 | optional: true 2101 | 2102 | "@next/swc-win32-arm64-msvc@14.2.14": 2103 | optional: true 2104 | 2105 | "@next/swc-win32-ia32-msvc@14.2.14": 2106 | optional: true 2107 | 2108 | "@next/swc-win32-x64-msvc@14.2.14": 2109 | optional: true 2110 | 2111 | "@node-rs/argon2-android-arm-eabi@2.0.0": 2112 | optional: true 2113 | 2114 | "@node-rs/argon2-android-arm64@2.0.0": 2115 | optional: true 2116 | 2117 | "@node-rs/argon2-darwin-arm64@2.0.0": 2118 | optional: true 2119 | 2120 | "@node-rs/argon2-darwin-x64@2.0.0": 2121 | optional: true 2122 | 2123 | "@node-rs/argon2-freebsd-x64@2.0.0": 2124 | optional: true 2125 | 2126 | "@node-rs/argon2-linux-arm-gnueabihf@2.0.0": 2127 | optional: true 2128 | 2129 | "@node-rs/argon2-linux-arm64-gnu@2.0.0": 2130 | optional: true 2131 | 2132 | "@node-rs/argon2-linux-arm64-musl@2.0.0": 2133 | optional: true 2134 | 2135 | "@node-rs/argon2-linux-x64-gnu@2.0.0": 2136 | optional: true 2137 | 2138 | "@node-rs/argon2-linux-x64-musl@2.0.0": 2139 | optional: true 2140 | 2141 | "@node-rs/argon2-wasm32-wasi@2.0.0": 2142 | dependencies: 2143 | "@napi-rs/wasm-runtime": 0.2.5 2144 | optional: true 2145 | 2146 | "@node-rs/argon2-win32-arm64-msvc@2.0.0": 2147 | optional: true 2148 | 2149 | "@node-rs/argon2-win32-ia32-msvc@2.0.0": 2150 | optional: true 2151 | 2152 | "@node-rs/argon2-win32-x64-msvc@2.0.0": 2153 | optional: true 2154 | 2155 | "@node-rs/argon2@2.0.0": 2156 | optionalDependencies: 2157 | "@node-rs/argon2-android-arm-eabi": 2.0.0 2158 | "@node-rs/argon2-android-arm64": 2.0.0 2159 | "@node-rs/argon2-darwin-arm64": 2.0.0 2160 | "@node-rs/argon2-darwin-x64": 2.0.0 2161 | "@node-rs/argon2-freebsd-x64": 2.0.0 2162 | "@node-rs/argon2-linux-arm-gnueabihf": 2.0.0 2163 | "@node-rs/argon2-linux-arm64-gnu": 2.0.0 2164 | "@node-rs/argon2-linux-arm64-musl": 2.0.0 2165 | "@node-rs/argon2-linux-x64-gnu": 2.0.0 2166 | "@node-rs/argon2-linux-x64-musl": 2.0.0 2167 | "@node-rs/argon2-wasm32-wasi": 2.0.0 2168 | "@node-rs/argon2-win32-arm64-msvc": 2.0.0 2169 | "@node-rs/argon2-win32-ia32-msvc": 2.0.0 2170 | "@node-rs/argon2-win32-x64-msvc": 2.0.0 2171 | 2172 | "@nodelib/fs.scandir@2.1.5": 2173 | dependencies: 2174 | "@nodelib/fs.stat": 2.0.5 2175 | run-parallel: 1.2.0 2176 | 2177 | "@nodelib/fs.stat@2.0.5": {} 2178 | 2179 | "@nodelib/fs.walk@1.2.8": 2180 | dependencies: 2181 | "@nodelib/fs.scandir": 2.1.5 2182 | fastq: 1.17.1 2183 | 2184 | "@nolyfill/is-core-module@1.0.39": {} 2185 | 2186 | "@oslojs/asn1@1.0.0": 2187 | dependencies: 2188 | "@oslojs/binary": 1.0.0 2189 | 2190 | "@oslojs/binary@1.0.0": {} 2191 | 2192 | "@oslojs/crypto@1.0.0": 2193 | dependencies: 2194 | "@oslojs/asn1": 1.0.0 2195 | "@oslojs/binary": 1.0.0 2196 | 2197 | "@oslojs/crypto@1.0.1": 2198 | dependencies: 2199 | "@oslojs/asn1": 1.0.0 2200 | "@oslojs/binary": 1.0.0 2201 | 2202 | "@oslojs/encoding@1.0.0": {} 2203 | 2204 | "@oslojs/encoding@1.1.0": {} 2205 | 2206 | "@oslojs/otp@1.0.0": 2207 | dependencies: 2208 | "@oslojs/binary": 1.0.0 2209 | "@oslojs/crypto": 1.0.0 2210 | "@oslojs/encoding": 1.0.0 2211 | 2212 | "@pilcrowjs/db-query@0.0.2": {} 2213 | 2214 | "@pkgjs/parseargs@0.11.0": 2215 | optional: true 2216 | 2217 | "@rtsao/scc@1.1.0": {} 2218 | 2219 | "@rushstack/eslint-patch@1.10.4": {} 2220 | 2221 | "@swc/counter@0.1.3": {} 2222 | 2223 | "@swc/helpers@0.5.5": 2224 | dependencies: 2225 | "@swc/counter": 0.1.3 2226 | tslib: 2.7.0 2227 | 2228 | "@tybys/wasm-util@0.9.0": 2229 | dependencies: 2230 | tslib: 2.7.0 2231 | optional: true 2232 | 2233 | "@types/better-sqlite3@7.6.11": 2234 | dependencies: 2235 | "@types/node": 20.16.10 2236 | 2237 | "@types/json5@0.0.29": {} 2238 | 2239 | "@types/node@20.16.10": 2240 | dependencies: 2241 | undici-types: 6.19.8 2242 | 2243 | "@types/prop-types@15.7.13": {} 2244 | 2245 | "@types/react-dom@18.3.0": 2246 | dependencies: 2247 | "@types/react": 18.3.11 2248 | 2249 | "@types/react@18.3.11": 2250 | dependencies: 2251 | "@types/prop-types": 15.7.13 2252 | csstype: 3.1.3 2253 | 2254 | "@typescript-eslint/eslint-plugin@8.8.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2)": 2255 | dependencies: 2256 | "@eslint-community/regexpp": 4.11.1 2257 | "@typescript-eslint/parser": 8.8.0(eslint@8.57.1)(typescript@5.6.2) 2258 | "@typescript-eslint/scope-manager": 8.8.0 2259 | "@typescript-eslint/type-utils": 8.8.0(eslint@8.57.1)(typescript@5.6.2) 2260 | "@typescript-eslint/utils": 8.8.0(eslint@8.57.1)(typescript@5.6.2) 2261 | "@typescript-eslint/visitor-keys": 8.8.0 2262 | eslint: 8.57.1 2263 | graphemer: 1.4.0 2264 | ignore: 5.3.2 2265 | natural-compare: 1.4.0 2266 | ts-api-utils: 1.3.0(typescript@5.6.2) 2267 | optionalDependencies: 2268 | typescript: 5.6.2 2269 | transitivePeerDependencies: 2270 | - supports-color 2271 | 2272 | "@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2)": 2273 | dependencies: 2274 | "@typescript-eslint/scope-manager": 8.8.0 2275 | "@typescript-eslint/types": 8.8.0 2276 | "@typescript-eslint/typescript-estree": 8.8.0(typescript@5.6.2) 2277 | "@typescript-eslint/visitor-keys": 8.8.0 2278 | debug: 4.3.7 2279 | eslint: 8.57.1 2280 | optionalDependencies: 2281 | typescript: 5.6.2 2282 | transitivePeerDependencies: 2283 | - supports-color 2284 | 2285 | "@typescript-eslint/scope-manager@8.8.0": 2286 | dependencies: 2287 | "@typescript-eslint/types": 8.8.0 2288 | "@typescript-eslint/visitor-keys": 8.8.0 2289 | 2290 | "@typescript-eslint/type-utils@8.8.0(eslint@8.57.1)(typescript@5.6.2)": 2291 | dependencies: 2292 | "@typescript-eslint/typescript-estree": 8.8.0(typescript@5.6.2) 2293 | "@typescript-eslint/utils": 8.8.0(eslint@8.57.1)(typescript@5.6.2) 2294 | debug: 4.3.7 2295 | ts-api-utils: 1.3.0(typescript@5.6.2) 2296 | optionalDependencies: 2297 | typescript: 5.6.2 2298 | transitivePeerDependencies: 2299 | - eslint 2300 | - supports-color 2301 | 2302 | "@typescript-eslint/types@8.8.0": {} 2303 | 2304 | "@typescript-eslint/typescript-estree@8.8.0(typescript@5.6.2)": 2305 | dependencies: 2306 | "@typescript-eslint/types": 8.8.0 2307 | "@typescript-eslint/visitor-keys": 8.8.0 2308 | debug: 4.3.7 2309 | fast-glob: 3.3.2 2310 | is-glob: 4.0.3 2311 | minimatch: 9.0.5 2312 | semver: 7.6.3 2313 | ts-api-utils: 1.3.0(typescript@5.6.2) 2314 | optionalDependencies: 2315 | typescript: 5.6.2 2316 | transitivePeerDependencies: 2317 | - supports-color 2318 | 2319 | "@typescript-eslint/utils@8.8.0(eslint@8.57.1)(typescript@5.6.2)": 2320 | dependencies: 2321 | "@eslint-community/eslint-utils": 4.4.0(eslint@8.57.1) 2322 | "@typescript-eslint/scope-manager": 8.8.0 2323 | "@typescript-eslint/types": 8.8.0 2324 | "@typescript-eslint/typescript-estree": 8.8.0(typescript@5.6.2) 2325 | eslint: 8.57.1 2326 | transitivePeerDependencies: 2327 | - supports-color 2328 | - typescript 2329 | 2330 | "@typescript-eslint/visitor-keys@8.8.0": 2331 | dependencies: 2332 | "@typescript-eslint/types": 8.8.0 2333 | eslint-visitor-keys: 3.4.3 2334 | 2335 | "@ungap/structured-clone@1.2.0": {} 2336 | 2337 | acorn-jsx@5.3.2(acorn@8.12.1): 2338 | dependencies: 2339 | acorn: 8.12.1 2340 | 2341 | acorn@8.12.1: {} 2342 | 2343 | ajv@6.12.6: 2344 | dependencies: 2345 | fast-deep-equal: 3.1.3 2346 | fast-json-stable-stringify: 2.1.0 2347 | json-schema-traverse: 0.4.1 2348 | uri-js: 4.4.1 2349 | 2350 | ansi-regex@5.0.1: {} 2351 | 2352 | ansi-regex@6.1.0: {} 2353 | 2354 | ansi-styles@4.3.0: 2355 | dependencies: 2356 | color-convert: 2.0.1 2357 | 2358 | ansi-styles@6.2.1: {} 2359 | 2360 | argparse@2.0.1: {} 2361 | 2362 | aria-query@5.1.3: 2363 | dependencies: 2364 | deep-equal: 2.2.3 2365 | 2366 | array-buffer-byte-length@1.0.1: 2367 | dependencies: 2368 | call-bind: 1.0.7 2369 | is-array-buffer: 3.0.4 2370 | 2371 | array-includes@3.1.8: 2372 | dependencies: 2373 | call-bind: 1.0.7 2374 | define-properties: 1.2.1 2375 | es-abstract: 1.23.3 2376 | es-object-atoms: 1.0.0 2377 | get-intrinsic: 1.2.4 2378 | is-string: 1.0.7 2379 | 2380 | array.prototype.findlast@1.2.5: 2381 | dependencies: 2382 | call-bind: 1.0.7 2383 | define-properties: 1.2.1 2384 | es-abstract: 1.23.3 2385 | es-errors: 1.3.0 2386 | es-object-atoms: 1.0.0 2387 | es-shim-unscopables: 1.0.2 2388 | 2389 | array.prototype.findlastindex@1.2.5: 2390 | dependencies: 2391 | call-bind: 1.0.7 2392 | define-properties: 1.2.1 2393 | es-abstract: 1.23.3 2394 | es-errors: 1.3.0 2395 | es-object-atoms: 1.0.0 2396 | es-shim-unscopables: 1.0.2 2397 | 2398 | array.prototype.flat@1.3.2: 2399 | dependencies: 2400 | call-bind: 1.0.7 2401 | define-properties: 1.2.1 2402 | es-abstract: 1.23.3 2403 | es-shim-unscopables: 1.0.2 2404 | 2405 | array.prototype.flatmap@1.3.2: 2406 | dependencies: 2407 | call-bind: 1.0.7 2408 | define-properties: 1.2.1 2409 | es-abstract: 1.23.3 2410 | es-shim-unscopables: 1.0.2 2411 | 2412 | array.prototype.tosorted@1.1.4: 2413 | dependencies: 2414 | call-bind: 1.0.7 2415 | define-properties: 1.2.1 2416 | es-abstract: 1.23.3 2417 | es-errors: 1.3.0 2418 | es-shim-unscopables: 1.0.2 2419 | 2420 | arraybuffer.prototype.slice@1.0.3: 2421 | dependencies: 2422 | array-buffer-byte-length: 1.0.1 2423 | call-bind: 1.0.7 2424 | define-properties: 1.2.1 2425 | es-abstract: 1.23.3 2426 | es-errors: 1.3.0 2427 | get-intrinsic: 1.2.4 2428 | is-array-buffer: 3.0.4 2429 | is-shared-array-buffer: 1.0.3 2430 | 2431 | ast-types-flow@0.0.8: {} 2432 | 2433 | available-typed-arrays@1.0.7: 2434 | dependencies: 2435 | possible-typed-array-names: 1.0.0 2436 | 2437 | axe-core@4.10.0: {} 2438 | 2439 | axobject-query@4.1.0: {} 2440 | 2441 | balanced-match@1.0.2: {} 2442 | 2443 | base64-js@1.5.1: {} 2444 | 2445 | better-sqlite3@11.3.0: 2446 | dependencies: 2447 | bindings: 1.5.0 2448 | prebuild-install: 7.1.2 2449 | 2450 | bindings@1.5.0: 2451 | dependencies: 2452 | file-uri-to-path: 1.0.0 2453 | 2454 | bl@4.1.0: 2455 | dependencies: 2456 | buffer: 5.7.1 2457 | inherits: 2.0.4 2458 | readable-stream: 3.6.2 2459 | 2460 | brace-expansion@1.1.11: 2461 | dependencies: 2462 | balanced-match: 1.0.2 2463 | concat-map: 0.0.1 2464 | 2465 | brace-expansion@2.0.1: 2466 | dependencies: 2467 | balanced-match: 1.0.2 2468 | 2469 | braces@3.0.3: 2470 | dependencies: 2471 | fill-range: 7.1.1 2472 | 2473 | buffer@5.7.1: 2474 | dependencies: 2475 | base64-js: 1.5.1 2476 | ieee754: 1.2.1 2477 | 2478 | busboy@1.6.0: 2479 | dependencies: 2480 | streamsearch: 1.1.0 2481 | 2482 | call-bind@1.0.7: 2483 | dependencies: 2484 | es-define-property: 1.0.0 2485 | es-errors: 1.3.0 2486 | function-bind: 1.1.2 2487 | get-intrinsic: 1.2.4 2488 | set-function-length: 1.2.2 2489 | 2490 | callsites@3.1.0: {} 2491 | 2492 | caniuse-lite@1.0.30001667: {} 2493 | 2494 | chalk@4.1.2: 2495 | dependencies: 2496 | ansi-styles: 4.3.0 2497 | supports-color: 7.2.0 2498 | 2499 | chownr@1.1.4: {} 2500 | 2501 | client-only@0.0.1: {} 2502 | 2503 | color-convert@2.0.1: 2504 | dependencies: 2505 | color-name: 1.1.4 2506 | 2507 | color-name@1.1.4: {} 2508 | 2509 | concat-map@0.0.1: {} 2510 | 2511 | cookie-es@1.2.2: {} 2512 | 2513 | cross-spawn@7.0.3: 2514 | dependencies: 2515 | path-key: 3.1.1 2516 | shebang-command: 2.0.0 2517 | which: 2.0.2 2518 | 2519 | csstype@3.1.3: {} 2520 | 2521 | damerau-levenshtein@1.0.8: {} 2522 | 2523 | data-view-buffer@1.0.1: 2524 | dependencies: 2525 | call-bind: 1.0.7 2526 | es-errors: 1.3.0 2527 | is-data-view: 1.0.1 2528 | 2529 | data-view-byte-length@1.0.1: 2530 | dependencies: 2531 | call-bind: 1.0.7 2532 | es-errors: 1.3.0 2533 | is-data-view: 1.0.1 2534 | 2535 | data-view-byte-offset@1.0.0: 2536 | dependencies: 2537 | call-bind: 1.0.7 2538 | es-errors: 1.3.0 2539 | is-data-view: 1.0.1 2540 | 2541 | debug@3.2.7: 2542 | dependencies: 2543 | ms: 2.1.3 2544 | 2545 | debug@4.3.7: 2546 | dependencies: 2547 | ms: 2.1.3 2548 | 2549 | decompress-response@6.0.0: 2550 | dependencies: 2551 | mimic-response: 3.1.0 2552 | 2553 | deep-equal@2.2.3: 2554 | dependencies: 2555 | array-buffer-byte-length: 1.0.1 2556 | call-bind: 1.0.7 2557 | es-get-iterator: 1.1.3 2558 | get-intrinsic: 1.2.4 2559 | is-arguments: 1.1.1 2560 | is-array-buffer: 3.0.4 2561 | is-date-object: 1.0.5 2562 | is-regex: 1.1.4 2563 | is-shared-array-buffer: 1.0.3 2564 | isarray: 2.0.5 2565 | object-is: 1.1.6 2566 | object-keys: 1.1.1 2567 | object.assign: 4.1.5 2568 | regexp.prototype.flags: 1.5.3 2569 | side-channel: 1.0.6 2570 | which-boxed-primitive: 1.0.2 2571 | which-collection: 1.0.2 2572 | which-typed-array: 1.1.15 2573 | 2574 | deep-extend@0.6.0: {} 2575 | 2576 | deep-is@0.1.4: {} 2577 | 2578 | define-data-property@1.1.4: 2579 | dependencies: 2580 | es-define-property: 1.0.0 2581 | es-errors: 1.3.0 2582 | gopd: 1.0.1 2583 | 2584 | define-properties@1.2.1: 2585 | dependencies: 2586 | define-data-property: 1.1.4 2587 | has-property-descriptors: 1.0.2 2588 | object-keys: 1.1.1 2589 | 2590 | detect-libc@2.0.3: {} 2591 | 2592 | doctrine@2.1.0: 2593 | dependencies: 2594 | esutils: 2.0.3 2595 | 2596 | doctrine@3.0.0: 2597 | dependencies: 2598 | esutils: 2.0.3 2599 | 2600 | eastasianwidth@0.2.0: {} 2601 | 2602 | emoji-regex@8.0.0: {} 2603 | 2604 | emoji-regex@9.2.2: {} 2605 | 2606 | end-of-stream@1.4.4: 2607 | dependencies: 2608 | once: 1.4.0 2609 | 2610 | enhanced-resolve@5.17.1: 2611 | dependencies: 2612 | graceful-fs: 4.2.11 2613 | tapable: 2.2.1 2614 | 2615 | es-abstract@1.23.3: 2616 | dependencies: 2617 | array-buffer-byte-length: 1.0.1 2618 | arraybuffer.prototype.slice: 1.0.3 2619 | available-typed-arrays: 1.0.7 2620 | call-bind: 1.0.7 2621 | data-view-buffer: 1.0.1 2622 | data-view-byte-length: 1.0.1 2623 | data-view-byte-offset: 1.0.0 2624 | es-define-property: 1.0.0 2625 | es-errors: 1.3.0 2626 | es-object-atoms: 1.0.0 2627 | es-set-tostringtag: 2.0.3 2628 | es-to-primitive: 1.2.1 2629 | function.prototype.name: 1.1.6 2630 | get-intrinsic: 1.2.4 2631 | get-symbol-description: 1.0.2 2632 | globalthis: 1.0.4 2633 | gopd: 1.0.1 2634 | has-property-descriptors: 1.0.2 2635 | has-proto: 1.0.3 2636 | has-symbols: 1.0.3 2637 | hasown: 2.0.2 2638 | internal-slot: 1.0.7 2639 | is-array-buffer: 3.0.4 2640 | is-callable: 1.2.7 2641 | is-data-view: 1.0.1 2642 | is-negative-zero: 2.0.3 2643 | is-regex: 1.1.4 2644 | is-shared-array-buffer: 1.0.3 2645 | is-string: 1.0.7 2646 | is-typed-array: 1.1.13 2647 | is-weakref: 1.0.2 2648 | object-inspect: 1.13.2 2649 | object-keys: 1.1.1 2650 | object.assign: 4.1.5 2651 | regexp.prototype.flags: 1.5.3 2652 | safe-array-concat: 1.1.2 2653 | safe-regex-test: 1.0.3 2654 | string.prototype.trim: 1.2.9 2655 | string.prototype.trimend: 1.0.8 2656 | string.prototype.trimstart: 1.0.8 2657 | typed-array-buffer: 1.0.2 2658 | typed-array-byte-length: 1.0.1 2659 | typed-array-byte-offset: 1.0.2 2660 | typed-array-length: 1.0.6 2661 | unbox-primitive: 1.0.2 2662 | which-typed-array: 1.1.15 2663 | 2664 | es-define-property@1.0.0: 2665 | dependencies: 2666 | get-intrinsic: 1.2.4 2667 | 2668 | es-errors@1.3.0: {} 2669 | 2670 | es-get-iterator@1.1.3: 2671 | dependencies: 2672 | call-bind: 1.0.7 2673 | get-intrinsic: 1.2.4 2674 | has-symbols: 1.0.3 2675 | is-arguments: 1.1.1 2676 | is-map: 2.0.3 2677 | is-set: 2.0.3 2678 | is-string: 1.0.7 2679 | isarray: 2.0.5 2680 | stop-iteration-iterator: 1.0.0 2681 | 2682 | es-iterator-helpers@1.0.19: 2683 | dependencies: 2684 | call-bind: 1.0.7 2685 | define-properties: 1.2.1 2686 | es-abstract: 1.23.3 2687 | es-errors: 1.3.0 2688 | es-set-tostringtag: 2.0.3 2689 | function-bind: 1.1.2 2690 | get-intrinsic: 1.2.4 2691 | globalthis: 1.0.4 2692 | has-property-descriptors: 1.0.2 2693 | has-proto: 1.0.3 2694 | has-symbols: 1.0.3 2695 | internal-slot: 1.0.7 2696 | iterator.prototype: 1.1.2 2697 | safe-array-concat: 1.1.2 2698 | 2699 | es-object-atoms@1.0.0: 2700 | dependencies: 2701 | es-errors: 1.3.0 2702 | 2703 | es-set-tostringtag@2.0.3: 2704 | dependencies: 2705 | get-intrinsic: 1.2.4 2706 | has-tostringtag: 1.0.2 2707 | hasown: 2.0.2 2708 | 2709 | es-shim-unscopables@1.0.2: 2710 | dependencies: 2711 | hasown: 2.0.2 2712 | 2713 | es-to-primitive@1.2.1: 2714 | dependencies: 2715 | is-callable: 1.2.7 2716 | is-date-object: 1.0.5 2717 | is-symbol: 1.0.4 2718 | 2719 | escape-string-regexp@4.0.0: {} 2720 | 2721 | eslint-config-next@14.2.14(eslint@8.57.1)(typescript@5.6.2): 2722 | dependencies: 2723 | "@next/eslint-plugin-next": 14.2.14 2724 | "@rushstack/eslint-patch": 1.10.4 2725 | "@typescript-eslint/eslint-plugin": 8.8.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2) 2726 | "@typescript-eslint/parser": 8.8.0(eslint@8.57.1)(typescript@5.6.2) 2727 | eslint: 8.57.1 2728 | eslint-import-resolver-node: 0.3.9 2729 | eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1) 2730 | eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) 2731 | eslint-plugin-jsx-a11y: 6.10.0(eslint@8.57.1) 2732 | eslint-plugin-react: 7.37.1(eslint@8.57.1) 2733 | eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1) 2734 | optionalDependencies: 2735 | typescript: 5.6.2 2736 | transitivePeerDependencies: 2737 | - eslint-import-resolver-webpack 2738 | - eslint-plugin-import-x 2739 | - supports-color 2740 | 2741 | eslint-config-prettier@9.1.0(eslint@8.57.1): 2742 | dependencies: 2743 | eslint: 8.57.1 2744 | 2745 | eslint-import-resolver-node@0.3.9: 2746 | dependencies: 2747 | debug: 3.2.7 2748 | is-core-module: 2.15.1 2749 | resolve: 1.22.8 2750 | transitivePeerDependencies: 2751 | - supports-color 2752 | 2753 | eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1): 2754 | dependencies: 2755 | "@nolyfill/is-core-module": 1.0.39 2756 | debug: 4.3.7 2757 | enhanced-resolve: 5.17.1 2758 | eslint: 8.57.1 2759 | eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) 2760 | fast-glob: 3.3.2 2761 | get-tsconfig: 4.8.1 2762 | is-bun-module: 1.2.1 2763 | is-glob: 4.0.3 2764 | optionalDependencies: 2765 | eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1) 2766 | transitivePeerDependencies: 2767 | - "@typescript-eslint/parser" 2768 | - eslint-import-resolver-node 2769 | - eslint-import-resolver-webpack 2770 | - supports-color 2771 | 2772 | eslint-module-utils@2.12.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): 2773 | dependencies: 2774 | debug: 3.2.7 2775 | optionalDependencies: 2776 | "@typescript-eslint/parser": 8.8.0(eslint@8.57.1)(typescript@5.6.2) 2777 | eslint: 8.57.1 2778 | eslint-import-resolver-node: 0.3.9 2779 | eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1) 2780 | transitivePeerDependencies: 2781 | - supports-color 2782 | 2783 | eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1): 2784 | dependencies: 2785 | "@rtsao/scc": 1.1.0 2786 | array-includes: 3.1.8 2787 | array.prototype.findlastindex: 1.2.5 2788 | array.prototype.flat: 1.3.2 2789 | array.prototype.flatmap: 1.3.2 2790 | debug: 3.2.7 2791 | doctrine: 2.1.0 2792 | eslint: 8.57.1 2793 | eslint-import-resolver-node: 0.3.9 2794 | eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) 2795 | hasown: 2.0.2 2796 | is-core-module: 2.15.1 2797 | is-glob: 4.0.3 2798 | minimatch: 3.1.2 2799 | object.fromentries: 2.0.8 2800 | object.groupby: 1.0.3 2801 | object.values: 1.2.0 2802 | semver: 6.3.1 2803 | string.prototype.trimend: 1.0.8 2804 | tsconfig-paths: 3.15.0 2805 | optionalDependencies: 2806 | "@typescript-eslint/parser": 8.8.0(eslint@8.57.1)(typescript@5.6.2) 2807 | transitivePeerDependencies: 2808 | - eslint-import-resolver-typescript 2809 | - eslint-import-resolver-webpack 2810 | - supports-color 2811 | 2812 | eslint-plugin-jsx-a11y@6.10.0(eslint@8.57.1): 2813 | dependencies: 2814 | aria-query: 5.1.3 2815 | array-includes: 3.1.8 2816 | array.prototype.flatmap: 1.3.2 2817 | ast-types-flow: 0.0.8 2818 | axe-core: 4.10.0 2819 | axobject-query: 4.1.0 2820 | damerau-levenshtein: 1.0.8 2821 | emoji-regex: 9.2.2 2822 | es-iterator-helpers: 1.0.19 2823 | eslint: 8.57.1 2824 | hasown: 2.0.2 2825 | jsx-ast-utils: 3.3.5 2826 | language-tags: 1.0.9 2827 | minimatch: 3.1.2 2828 | object.fromentries: 2.0.8 2829 | safe-regex-test: 1.0.3 2830 | string.prototype.includes: 2.0.0 2831 | 2832 | eslint-plugin-react-hooks@4.6.2(eslint@8.57.1): 2833 | dependencies: 2834 | eslint: 8.57.1 2835 | 2836 | eslint-plugin-react@7.37.1(eslint@8.57.1): 2837 | dependencies: 2838 | array-includes: 3.1.8 2839 | array.prototype.findlast: 1.2.5 2840 | array.prototype.flatmap: 1.3.2 2841 | array.prototype.tosorted: 1.1.4 2842 | doctrine: 2.1.0 2843 | es-iterator-helpers: 1.0.19 2844 | eslint: 8.57.1 2845 | estraverse: 5.3.0 2846 | hasown: 2.0.2 2847 | jsx-ast-utils: 3.3.5 2848 | minimatch: 3.1.2 2849 | object.entries: 1.1.8 2850 | object.fromentries: 2.0.8 2851 | object.values: 1.2.0 2852 | prop-types: 15.8.1 2853 | resolve: 2.0.0-next.5 2854 | semver: 6.3.1 2855 | string.prototype.matchall: 4.0.11 2856 | string.prototype.repeat: 1.0.0 2857 | 2858 | eslint-scope@7.2.2: 2859 | dependencies: 2860 | esrecurse: 4.3.0 2861 | estraverse: 5.3.0 2862 | 2863 | eslint-visitor-keys@3.4.3: {} 2864 | 2865 | eslint@8.57.1: 2866 | dependencies: 2867 | "@eslint-community/eslint-utils": 4.4.0(eslint@8.57.1) 2868 | "@eslint-community/regexpp": 4.11.1 2869 | "@eslint/eslintrc": 2.1.4 2870 | "@eslint/js": 8.57.1 2871 | "@humanwhocodes/config-array": 0.13.0 2872 | "@humanwhocodes/module-importer": 1.0.1 2873 | "@nodelib/fs.walk": 1.2.8 2874 | "@ungap/structured-clone": 1.2.0 2875 | ajv: 6.12.6 2876 | chalk: 4.1.2 2877 | cross-spawn: 7.0.3 2878 | debug: 4.3.7 2879 | doctrine: 3.0.0 2880 | escape-string-regexp: 4.0.0 2881 | eslint-scope: 7.2.2 2882 | eslint-visitor-keys: 3.4.3 2883 | espree: 9.6.1 2884 | esquery: 1.6.0 2885 | esutils: 2.0.3 2886 | fast-deep-equal: 3.1.3 2887 | file-entry-cache: 6.0.1 2888 | find-up: 5.0.0 2889 | glob-parent: 6.0.2 2890 | globals: 13.24.0 2891 | graphemer: 1.4.0 2892 | ignore: 5.3.2 2893 | imurmurhash: 0.1.4 2894 | is-glob: 4.0.3 2895 | is-path-inside: 3.0.3 2896 | js-yaml: 4.1.0 2897 | json-stable-stringify-without-jsonify: 1.0.1 2898 | levn: 0.4.1 2899 | lodash.merge: 4.6.2 2900 | minimatch: 3.1.2 2901 | natural-compare: 1.4.0 2902 | optionator: 0.9.4 2903 | strip-ansi: 6.0.1 2904 | text-table: 0.2.0 2905 | transitivePeerDependencies: 2906 | - supports-color 2907 | 2908 | espree@9.6.1: 2909 | dependencies: 2910 | acorn: 8.12.1 2911 | acorn-jsx: 5.3.2(acorn@8.12.1) 2912 | eslint-visitor-keys: 3.4.3 2913 | 2914 | esquery@1.6.0: 2915 | dependencies: 2916 | estraverse: 5.3.0 2917 | 2918 | esrecurse@4.3.0: 2919 | dependencies: 2920 | estraverse: 5.3.0 2921 | 2922 | estraverse@5.3.0: {} 2923 | 2924 | esutils@2.0.3: {} 2925 | 2926 | expand-template@2.0.3: {} 2927 | 2928 | fast-deep-equal@3.1.3: {} 2929 | 2930 | fast-glob@3.3.2: 2931 | dependencies: 2932 | "@nodelib/fs.stat": 2.0.5 2933 | "@nodelib/fs.walk": 1.2.8 2934 | glob-parent: 5.1.2 2935 | merge2: 1.4.1 2936 | micromatch: 4.0.8 2937 | 2938 | fast-json-stable-stringify@2.1.0: {} 2939 | 2940 | fast-levenshtein@2.0.6: {} 2941 | 2942 | fastq@1.17.1: 2943 | dependencies: 2944 | reusify: 1.0.4 2945 | 2946 | file-entry-cache@6.0.1: 2947 | dependencies: 2948 | flat-cache: 3.2.0 2949 | 2950 | file-uri-to-path@1.0.0: {} 2951 | 2952 | fill-range@7.1.1: 2953 | dependencies: 2954 | to-regex-range: 5.0.1 2955 | 2956 | find-up@5.0.0: 2957 | dependencies: 2958 | locate-path: 6.0.0 2959 | path-exists: 4.0.0 2960 | 2961 | flat-cache@3.2.0: 2962 | dependencies: 2963 | flatted: 3.3.1 2964 | keyv: 4.5.4 2965 | rimraf: 3.0.2 2966 | 2967 | flatted@3.3.1: {} 2968 | 2969 | for-each@0.3.3: 2970 | dependencies: 2971 | is-callable: 1.2.7 2972 | 2973 | foreground-child@3.3.0: 2974 | dependencies: 2975 | cross-spawn: 7.0.3 2976 | signal-exit: 4.1.0 2977 | 2978 | fs-constants@1.0.0: {} 2979 | 2980 | fs.realpath@1.0.0: {} 2981 | 2982 | function-bind@1.1.2: {} 2983 | 2984 | function.prototype.name@1.1.6: 2985 | dependencies: 2986 | call-bind: 1.0.7 2987 | define-properties: 1.2.1 2988 | es-abstract: 1.23.3 2989 | functions-have-names: 1.2.3 2990 | 2991 | functions-have-names@1.2.3: {} 2992 | 2993 | get-intrinsic@1.2.4: 2994 | dependencies: 2995 | es-errors: 1.3.0 2996 | function-bind: 1.1.2 2997 | has-proto: 1.0.3 2998 | has-symbols: 1.0.3 2999 | hasown: 2.0.2 3000 | 3001 | get-symbol-description@1.0.2: 3002 | dependencies: 3003 | call-bind: 1.0.7 3004 | es-errors: 1.3.0 3005 | get-intrinsic: 1.2.4 3006 | 3007 | get-tsconfig@4.8.1: 3008 | dependencies: 3009 | resolve-pkg-maps: 1.0.0 3010 | 3011 | github-from-package@0.0.0: {} 3012 | 3013 | glob-parent@5.1.2: 3014 | dependencies: 3015 | is-glob: 4.0.3 3016 | 3017 | glob-parent@6.0.2: 3018 | dependencies: 3019 | is-glob: 4.0.3 3020 | 3021 | glob@10.3.10: 3022 | dependencies: 3023 | foreground-child: 3.3.0 3024 | jackspeak: 2.3.6 3025 | minimatch: 9.0.5 3026 | minipass: 7.1.2 3027 | path-scurry: 1.11.1 3028 | 3029 | glob@7.2.3: 3030 | dependencies: 3031 | fs.realpath: 1.0.0 3032 | inflight: 1.0.6 3033 | inherits: 2.0.4 3034 | minimatch: 3.1.2 3035 | once: 1.4.0 3036 | path-is-absolute: 1.0.1 3037 | 3038 | globals@13.24.0: 3039 | dependencies: 3040 | type-fest: 0.20.2 3041 | 3042 | globalthis@1.0.4: 3043 | dependencies: 3044 | define-properties: 1.2.1 3045 | gopd: 1.0.1 3046 | 3047 | gopd@1.0.1: 3048 | dependencies: 3049 | get-intrinsic: 1.2.4 3050 | 3051 | graceful-fs@4.2.11: {} 3052 | 3053 | graphemer@1.4.0: {} 3054 | 3055 | has-bigints@1.0.2: {} 3056 | 3057 | has-flag@4.0.0: {} 3058 | 3059 | has-property-descriptors@1.0.2: 3060 | dependencies: 3061 | es-define-property: 1.0.0 3062 | 3063 | has-proto@1.0.3: {} 3064 | 3065 | has-symbols@1.0.3: {} 3066 | 3067 | has-tostringtag@1.0.2: 3068 | dependencies: 3069 | has-symbols: 1.0.3 3070 | 3071 | hasown@2.0.2: 3072 | dependencies: 3073 | function-bind: 1.1.2 3074 | 3075 | ieee754@1.2.1: {} 3076 | 3077 | ignore@5.3.2: {} 3078 | 3079 | import-fresh@3.3.0: 3080 | dependencies: 3081 | parent-module: 1.0.1 3082 | resolve-from: 4.0.0 3083 | 3084 | imurmurhash@0.1.4: {} 3085 | 3086 | inflight@1.0.6: 3087 | dependencies: 3088 | once: 1.4.0 3089 | wrappy: 1.0.2 3090 | 3091 | inherits@2.0.4: {} 3092 | 3093 | ini@1.3.8: {} 3094 | 3095 | internal-slot@1.0.7: 3096 | dependencies: 3097 | es-errors: 1.3.0 3098 | hasown: 2.0.2 3099 | side-channel: 1.0.6 3100 | 3101 | is-arguments@1.1.1: 3102 | dependencies: 3103 | call-bind: 1.0.7 3104 | has-tostringtag: 1.0.2 3105 | 3106 | is-array-buffer@3.0.4: 3107 | dependencies: 3108 | call-bind: 1.0.7 3109 | get-intrinsic: 1.2.4 3110 | 3111 | is-async-function@2.0.0: 3112 | dependencies: 3113 | has-tostringtag: 1.0.2 3114 | 3115 | is-bigint@1.0.4: 3116 | dependencies: 3117 | has-bigints: 1.0.2 3118 | 3119 | is-boolean-object@1.1.2: 3120 | dependencies: 3121 | call-bind: 1.0.7 3122 | has-tostringtag: 1.0.2 3123 | 3124 | is-bun-module@1.2.1: 3125 | dependencies: 3126 | semver: 7.6.3 3127 | 3128 | is-callable@1.2.7: {} 3129 | 3130 | is-core-module@2.15.1: 3131 | dependencies: 3132 | hasown: 2.0.2 3133 | 3134 | is-data-view@1.0.1: 3135 | dependencies: 3136 | is-typed-array: 1.1.13 3137 | 3138 | is-date-object@1.0.5: 3139 | dependencies: 3140 | has-tostringtag: 1.0.2 3141 | 3142 | is-extglob@2.1.1: {} 3143 | 3144 | is-finalizationregistry@1.0.2: 3145 | dependencies: 3146 | call-bind: 1.0.7 3147 | 3148 | is-fullwidth-code-point@3.0.0: {} 3149 | 3150 | is-generator-function@1.0.10: 3151 | dependencies: 3152 | has-tostringtag: 1.0.2 3153 | 3154 | is-glob@4.0.3: 3155 | dependencies: 3156 | is-extglob: 2.1.1 3157 | 3158 | is-map@2.0.3: {} 3159 | 3160 | is-negative-zero@2.0.3: {} 3161 | 3162 | is-number-object@1.0.7: 3163 | dependencies: 3164 | has-tostringtag: 1.0.2 3165 | 3166 | is-number@7.0.0: {} 3167 | 3168 | is-path-inside@3.0.3: {} 3169 | 3170 | is-regex@1.1.4: 3171 | dependencies: 3172 | call-bind: 1.0.7 3173 | has-tostringtag: 1.0.2 3174 | 3175 | is-set@2.0.3: {} 3176 | 3177 | is-shared-array-buffer@1.0.3: 3178 | dependencies: 3179 | call-bind: 1.0.7 3180 | 3181 | is-string@1.0.7: 3182 | dependencies: 3183 | has-tostringtag: 1.0.2 3184 | 3185 | is-symbol@1.0.4: 3186 | dependencies: 3187 | has-symbols: 1.0.3 3188 | 3189 | is-typed-array@1.1.13: 3190 | dependencies: 3191 | which-typed-array: 1.1.15 3192 | 3193 | is-weakmap@2.0.2: {} 3194 | 3195 | is-weakref@1.0.2: 3196 | dependencies: 3197 | call-bind: 1.0.7 3198 | 3199 | is-weakset@2.0.3: 3200 | dependencies: 3201 | call-bind: 1.0.7 3202 | get-intrinsic: 1.2.4 3203 | 3204 | isarray@2.0.5: {} 3205 | 3206 | isexe@2.0.0: {} 3207 | 3208 | iterator.prototype@1.1.2: 3209 | dependencies: 3210 | define-properties: 1.2.1 3211 | get-intrinsic: 1.2.4 3212 | has-symbols: 1.0.3 3213 | reflect.getprototypeof: 1.0.6 3214 | set-function-name: 2.0.2 3215 | 3216 | jackspeak@2.3.6: 3217 | dependencies: 3218 | "@isaacs/cliui": 8.0.2 3219 | optionalDependencies: 3220 | "@pkgjs/parseargs": 0.11.0 3221 | 3222 | js-tokens@4.0.0: {} 3223 | 3224 | js-yaml@4.1.0: 3225 | dependencies: 3226 | argparse: 2.0.1 3227 | 3228 | json-buffer@3.0.1: {} 3229 | 3230 | json-schema-traverse@0.4.1: {} 3231 | 3232 | json-stable-stringify-without-jsonify@1.0.1: {} 3233 | 3234 | json5@1.0.2: 3235 | dependencies: 3236 | minimist: 1.2.8 3237 | 3238 | jsx-ast-utils@3.3.5: 3239 | dependencies: 3240 | array-includes: 3.1.8 3241 | array.prototype.flat: 1.3.2 3242 | object.assign: 4.1.5 3243 | object.values: 1.2.0 3244 | 3245 | keyv@4.5.4: 3246 | dependencies: 3247 | json-buffer: 3.0.1 3248 | 3249 | language-subtag-registry@0.3.23: {} 3250 | 3251 | language-tags@1.0.9: 3252 | dependencies: 3253 | language-subtag-registry: 0.3.23 3254 | 3255 | levn@0.4.1: 3256 | dependencies: 3257 | prelude-ls: 1.2.1 3258 | type-check: 0.4.0 3259 | 3260 | locate-path@6.0.0: 3261 | dependencies: 3262 | p-locate: 5.0.0 3263 | 3264 | lodash.merge@4.6.2: {} 3265 | 3266 | loose-envify@1.4.0: 3267 | dependencies: 3268 | js-tokens: 4.0.0 3269 | 3270 | lru-cache@10.4.3: {} 3271 | 3272 | merge2@1.4.1: {} 3273 | 3274 | micromatch@4.0.8: 3275 | dependencies: 3276 | braces: 3.0.3 3277 | picomatch: 2.3.1 3278 | 3279 | mimic-response@3.1.0: {} 3280 | 3281 | minimatch@3.1.2: 3282 | dependencies: 3283 | brace-expansion: 1.1.11 3284 | 3285 | minimatch@9.0.5: 3286 | dependencies: 3287 | brace-expansion: 2.0.1 3288 | 3289 | minimist@1.2.8: {} 3290 | 3291 | minipass@7.1.2: {} 3292 | 3293 | mkdirp-classic@0.5.3: {} 3294 | 3295 | ms@2.1.3: {} 3296 | 3297 | nanoid@3.3.7: {} 3298 | 3299 | napi-build-utils@1.0.2: {} 3300 | 3301 | natural-compare@1.4.0: {} 3302 | 3303 | next@14.2.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1): 3304 | dependencies: 3305 | "@next/env": 14.2.14 3306 | "@swc/helpers": 0.5.5 3307 | busboy: 1.6.0 3308 | caniuse-lite: 1.0.30001667 3309 | graceful-fs: 4.2.11 3310 | postcss: 8.4.31 3311 | react: 18.3.1 3312 | react-dom: 18.3.1(react@18.3.1) 3313 | styled-jsx: 5.1.1(react@18.3.1) 3314 | optionalDependencies: 3315 | "@next/swc-darwin-arm64": 14.2.14 3316 | "@next/swc-darwin-x64": 14.2.14 3317 | "@next/swc-linux-arm64-gnu": 14.2.14 3318 | "@next/swc-linux-arm64-musl": 14.2.14 3319 | "@next/swc-linux-x64-gnu": 14.2.14 3320 | "@next/swc-linux-x64-musl": 14.2.14 3321 | "@next/swc-win32-arm64-msvc": 14.2.14 3322 | "@next/swc-win32-ia32-msvc": 14.2.14 3323 | "@next/swc-win32-x64-msvc": 14.2.14 3324 | transitivePeerDependencies: 3325 | - "@babel/core" 3326 | - babel-plugin-macros 3327 | 3328 | node-abi@3.68.0: 3329 | dependencies: 3330 | semver: 7.6.3 3331 | 3332 | object-assign@4.1.1: {} 3333 | 3334 | object-inspect@1.13.2: {} 3335 | 3336 | object-is@1.1.6: 3337 | dependencies: 3338 | call-bind: 1.0.7 3339 | define-properties: 1.2.1 3340 | 3341 | object-keys@1.1.1: {} 3342 | 3343 | object.assign@4.1.5: 3344 | dependencies: 3345 | call-bind: 1.0.7 3346 | define-properties: 1.2.1 3347 | has-symbols: 1.0.3 3348 | object-keys: 1.1.1 3349 | 3350 | object.entries@1.1.8: 3351 | dependencies: 3352 | call-bind: 1.0.7 3353 | define-properties: 1.2.1 3354 | es-object-atoms: 1.0.0 3355 | 3356 | object.fromentries@2.0.8: 3357 | dependencies: 3358 | call-bind: 1.0.7 3359 | define-properties: 1.2.1 3360 | es-abstract: 1.23.3 3361 | es-object-atoms: 1.0.0 3362 | 3363 | object.groupby@1.0.3: 3364 | dependencies: 3365 | call-bind: 1.0.7 3366 | define-properties: 1.2.1 3367 | es-abstract: 1.23.3 3368 | 3369 | object.values@1.2.0: 3370 | dependencies: 3371 | call-bind: 1.0.7 3372 | define-properties: 1.2.1 3373 | es-object-atoms: 1.0.0 3374 | 3375 | once@1.4.0: 3376 | dependencies: 3377 | wrappy: 1.0.2 3378 | 3379 | optionator@0.9.4: 3380 | dependencies: 3381 | deep-is: 0.1.4 3382 | fast-levenshtein: 2.0.6 3383 | levn: 0.4.1 3384 | prelude-ls: 1.2.1 3385 | type-check: 0.4.0 3386 | word-wrap: 1.2.5 3387 | 3388 | p-limit@3.1.0: 3389 | dependencies: 3390 | yocto-queue: 0.1.0 3391 | 3392 | p-locate@5.0.0: 3393 | dependencies: 3394 | p-limit: 3.1.0 3395 | 3396 | parent-module@1.0.1: 3397 | dependencies: 3398 | callsites: 3.1.0 3399 | 3400 | path-exists@4.0.0: {} 3401 | 3402 | path-is-absolute@1.0.1: {} 3403 | 3404 | path-key@3.1.1: {} 3405 | 3406 | path-parse@1.0.7: {} 3407 | 3408 | path-scurry@1.11.1: 3409 | dependencies: 3410 | lru-cache: 10.4.3 3411 | minipass: 7.1.2 3412 | 3413 | picocolors@1.1.0: {} 3414 | 3415 | picomatch@2.3.1: {} 3416 | 3417 | possible-typed-array-names@1.0.0: {} 3418 | 3419 | postcss@8.4.31: 3420 | dependencies: 3421 | nanoid: 3.3.7 3422 | picocolors: 1.1.0 3423 | source-map-js: 1.2.1 3424 | 3425 | prebuild-install@7.1.2: 3426 | dependencies: 3427 | detect-libc: 2.0.3 3428 | expand-template: 2.0.3 3429 | github-from-package: 0.0.0 3430 | minimist: 1.2.8 3431 | mkdirp-classic: 0.5.3 3432 | napi-build-utils: 1.0.2 3433 | node-abi: 3.68.0 3434 | pump: 3.0.2 3435 | rc: 1.2.8 3436 | simple-get: 4.0.1 3437 | tar-fs: 2.1.1 3438 | tunnel-agent: 0.6.0 3439 | 3440 | prelude-ls@1.2.1: {} 3441 | 3442 | prettier@3.3.3: {} 3443 | 3444 | prop-types@15.8.1: 3445 | dependencies: 3446 | loose-envify: 1.4.0 3447 | object-assign: 4.1.1 3448 | react-is: 16.13.1 3449 | 3450 | pump@3.0.2: 3451 | dependencies: 3452 | end-of-stream: 1.4.4 3453 | once: 1.4.0 3454 | 3455 | punycode@2.3.1: {} 3456 | 3457 | queue-microtask@1.2.3: {} 3458 | 3459 | rc@1.2.8: 3460 | dependencies: 3461 | deep-extend: 0.6.0 3462 | ini: 1.3.8 3463 | minimist: 1.2.8 3464 | strip-json-comments: 2.0.1 3465 | 3466 | react-dom@18.3.1(react@18.3.1): 3467 | dependencies: 3468 | loose-envify: 1.4.0 3469 | react: 18.3.1 3470 | scheduler: 0.23.2 3471 | 3472 | react-is@16.13.1: {} 3473 | 3474 | react@18.3.1: 3475 | dependencies: 3476 | loose-envify: 1.4.0 3477 | 3478 | readable-stream@3.6.2: 3479 | dependencies: 3480 | inherits: 2.0.4 3481 | string_decoder: 1.3.0 3482 | util-deprecate: 1.0.2 3483 | 3484 | reflect.getprototypeof@1.0.6: 3485 | dependencies: 3486 | call-bind: 1.0.7 3487 | define-properties: 1.2.1 3488 | es-abstract: 1.23.3 3489 | es-errors: 1.3.0 3490 | get-intrinsic: 1.2.4 3491 | globalthis: 1.0.4 3492 | which-builtin-type: 1.1.4 3493 | 3494 | regexp.prototype.flags@1.5.3: 3495 | dependencies: 3496 | call-bind: 1.0.7 3497 | define-properties: 1.2.1 3498 | es-errors: 1.3.0 3499 | set-function-name: 2.0.2 3500 | 3501 | resolve-from@4.0.0: {} 3502 | 3503 | resolve-pkg-maps@1.0.0: {} 3504 | 3505 | resolve@1.22.8: 3506 | dependencies: 3507 | is-core-module: 2.15.1 3508 | path-parse: 1.0.7 3509 | supports-preserve-symlinks-flag: 1.0.0 3510 | 3511 | resolve@2.0.0-next.5: 3512 | dependencies: 3513 | is-core-module: 2.15.1 3514 | path-parse: 1.0.7 3515 | supports-preserve-symlinks-flag: 1.0.0 3516 | 3517 | reusify@1.0.4: {} 3518 | 3519 | rimraf@3.0.2: 3520 | dependencies: 3521 | glob: 7.2.3 3522 | 3523 | run-parallel@1.2.0: 3524 | dependencies: 3525 | queue-microtask: 1.2.3 3526 | 3527 | safe-array-concat@1.1.2: 3528 | dependencies: 3529 | call-bind: 1.0.7 3530 | get-intrinsic: 1.2.4 3531 | has-symbols: 1.0.3 3532 | isarray: 2.0.5 3533 | 3534 | safe-buffer@5.2.1: {} 3535 | 3536 | safe-regex-test@1.0.3: 3537 | dependencies: 3538 | call-bind: 1.0.7 3539 | es-errors: 1.3.0 3540 | is-regex: 1.1.4 3541 | 3542 | scheduler@0.23.2: 3543 | dependencies: 3544 | loose-envify: 1.4.0 3545 | 3546 | semver@6.3.1: {} 3547 | 3548 | semver@7.6.3: {} 3549 | 3550 | set-function-length@1.2.2: 3551 | dependencies: 3552 | define-data-property: 1.1.4 3553 | es-errors: 1.3.0 3554 | function-bind: 1.1.2 3555 | get-intrinsic: 1.2.4 3556 | gopd: 1.0.1 3557 | has-property-descriptors: 1.0.2 3558 | 3559 | set-function-name@2.0.2: 3560 | dependencies: 3561 | define-data-property: 1.1.4 3562 | es-errors: 1.3.0 3563 | functions-have-names: 1.2.3 3564 | has-property-descriptors: 1.0.2 3565 | 3566 | shebang-command@2.0.0: 3567 | dependencies: 3568 | shebang-regex: 3.0.0 3569 | 3570 | shebang-regex@3.0.0: {} 3571 | 3572 | side-channel@1.0.6: 3573 | dependencies: 3574 | call-bind: 1.0.7 3575 | es-errors: 1.3.0 3576 | get-intrinsic: 1.2.4 3577 | object-inspect: 1.13.2 3578 | 3579 | signal-exit@4.1.0: {} 3580 | 3581 | simple-concat@1.0.1: {} 3582 | 3583 | simple-get@4.0.1: 3584 | dependencies: 3585 | decompress-response: 6.0.0 3586 | once: 1.4.0 3587 | simple-concat: 1.0.1 3588 | 3589 | source-map-js@1.2.1: {} 3590 | 3591 | stop-iteration-iterator@1.0.0: 3592 | dependencies: 3593 | internal-slot: 1.0.7 3594 | 3595 | streamsearch@1.1.0: {} 3596 | 3597 | string-width@4.2.3: 3598 | dependencies: 3599 | emoji-regex: 8.0.0 3600 | is-fullwidth-code-point: 3.0.0 3601 | strip-ansi: 6.0.1 3602 | 3603 | string-width@5.1.2: 3604 | dependencies: 3605 | eastasianwidth: 0.2.0 3606 | emoji-regex: 9.2.2 3607 | strip-ansi: 7.1.0 3608 | 3609 | string.prototype.includes@2.0.0: 3610 | dependencies: 3611 | define-properties: 1.2.1 3612 | es-abstract: 1.23.3 3613 | 3614 | string.prototype.matchall@4.0.11: 3615 | dependencies: 3616 | call-bind: 1.0.7 3617 | define-properties: 1.2.1 3618 | es-abstract: 1.23.3 3619 | es-errors: 1.3.0 3620 | es-object-atoms: 1.0.0 3621 | get-intrinsic: 1.2.4 3622 | gopd: 1.0.1 3623 | has-symbols: 1.0.3 3624 | internal-slot: 1.0.7 3625 | regexp.prototype.flags: 1.5.3 3626 | set-function-name: 2.0.2 3627 | side-channel: 1.0.6 3628 | 3629 | string.prototype.repeat@1.0.0: 3630 | dependencies: 3631 | define-properties: 1.2.1 3632 | es-abstract: 1.23.3 3633 | 3634 | string.prototype.trim@1.2.9: 3635 | dependencies: 3636 | call-bind: 1.0.7 3637 | define-properties: 1.2.1 3638 | es-abstract: 1.23.3 3639 | es-object-atoms: 1.0.0 3640 | 3641 | string.prototype.trimend@1.0.8: 3642 | dependencies: 3643 | call-bind: 1.0.7 3644 | define-properties: 1.2.1 3645 | es-object-atoms: 1.0.0 3646 | 3647 | string.prototype.trimstart@1.0.8: 3648 | dependencies: 3649 | call-bind: 1.0.7 3650 | define-properties: 1.2.1 3651 | es-object-atoms: 1.0.0 3652 | 3653 | string_decoder@1.3.0: 3654 | dependencies: 3655 | safe-buffer: 5.2.1 3656 | 3657 | strip-ansi@6.0.1: 3658 | dependencies: 3659 | ansi-regex: 5.0.1 3660 | 3661 | strip-ansi@7.1.0: 3662 | dependencies: 3663 | ansi-regex: 6.1.0 3664 | 3665 | strip-bom@3.0.0: {} 3666 | 3667 | strip-json-comments@2.0.1: {} 3668 | 3669 | strip-json-comments@3.1.1: {} 3670 | 3671 | styled-jsx@5.1.1(react@18.3.1): 3672 | dependencies: 3673 | client-only: 0.0.1 3674 | react: 18.3.1 3675 | 3676 | supports-color@7.2.0: 3677 | dependencies: 3678 | has-flag: 4.0.0 3679 | 3680 | supports-preserve-symlinks-flag@1.0.0: {} 3681 | 3682 | tapable@2.2.1: {} 3683 | 3684 | tar-fs@2.1.1: 3685 | dependencies: 3686 | chownr: 1.1.4 3687 | mkdirp-classic: 0.5.3 3688 | pump: 3.0.2 3689 | tar-stream: 2.2.0 3690 | 3691 | tar-stream@2.2.0: 3692 | dependencies: 3693 | bl: 4.1.0 3694 | end-of-stream: 1.4.4 3695 | fs-constants: 1.0.0 3696 | inherits: 2.0.4 3697 | readable-stream: 3.6.2 3698 | 3699 | text-table@0.2.0: {} 3700 | 3701 | to-regex-range@5.0.1: 3702 | dependencies: 3703 | is-number: 7.0.0 3704 | 3705 | ts-api-utils@1.3.0(typescript@5.6.2): 3706 | dependencies: 3707 | typescript: 5.6.2 3708 | 3709 | tsconfig-paths@3.15.0: 3710 | dependencies: 3711 | "@types/json5": 0.0.29 3712 | json5: 1.0.2 3713 | minimist: 1.2.8 3714 | strip-bom: 3.0.0 3715 | 3716 | tslib@2.7.0: {} 3717 | 3718 | tunnel-agent@0.6.0: 3719 | dependencies: 3720 | safe-buffer: 5.2.1 3721 | 3722 | type-check@0.4.0: 3723 | dependencies: 3724 | prelude-ls: 1.2.1 3725 | 3726 | type-fest@0.20.2: {} 3727 | 3728 | typed-array-buffer@1.0.2: 3729 | dependencies: 3730 | call-bind: 1.0.7 3731 | es-errors: 1.3.0 3732 | is-typed-array: 1.1.13 3733 | 3734 | typed-array-byte-length@1.0.1: 3735 | dependencies: 3736 | call-bind: 1.0.7 3737 | for-each: 0.3.3 3738 | gopd: 1.0.1 3739 | has-proto: 1.0.3 3740 | is-typed-array: 1.1.13 3741 | 3742 | typed-array-byte-offset@1.0.2: 3743 | dependencies: 3744 | available-typed-arrays: 1.0.7 3745 | call-bind: 1.0.7 3746 | for-each: 0.3.3 3747 | gopd: 1.0.1 3748 | has-proto: 1.0.3 3749 | is-typed-array: 1.1.13 3750 | 3751 | typed-array-length@1.0.6: 3752 | dependencies: 3753 | call-bind: 1.0.7 3754 | for-each: 0.3.3 3755 | gopd: 1.0.1 3756 | has-proto: 1.0.3 3757 | is-typed-array: 1.1.13 3758 | possible-typed-array-names: 1.0.0 3759 | 3760 | typescript@5.6.2: {} 3761 | 3762 | unbox-primitive@1.0.2: 3763 | dependencies: 3764 | call-bind: 1.0.7 3765 | has-bigints: 1.0.2 3766 | has-symbols: 1.0.3 3767 | which-boxed-primitive: 1.0.2 3768 | 3769 | undici-types@6.19.8: {} 3770 | 3771 | uqr@0.1.2: {} 3772 | 3773 | uri-js@4.4.1: 3774 | dependencies: 3775 | punycode: 2.3.1 3776 | 3777 | util-deprecate@1.0.2: {} 3778 | 3779 | which-boxed-primitive@1.0.2: 3780 | dependencies: 3781 | is-bigint: 1.0.4 3782 | is-boolean-object: 1.1.2 3783 | is-number-object: 1.0.7 3784 | is-string: 1.0.7 3785 | is-symbol: 1.0.4 3786 | 3787 | which-builtin-type@1.1.4: 3788 | dependencies: 3789 | function.prototype.name: 1.1.6 3790 | has-tostringtag: 1.0.2 3791 | is-async-function: 2.0.0 3792 | is-date-object: 1.0.5 3793 | is-finalizationregistry: 1.0.2 3794 | is-generator-function: 1.0.10 3795 | is-regex: 1.1.4 3796 | is-weakref: 1.0.2 3797 | isarray: 2.0.5 3798 | which-boxed-primitive: 1.0.2 3799 | which-collection: 1.0.2 3800 | which-typed-array: 1.1.15 3801 | 3802 | which-collection@1.0.2: 3803 | dependencies: 3804 | is-map: 2.0.3 3805 | is-set: 2.0.3 3806 | is-weakmap: 2.0.2 3807 | is-weakset: 2.0.3 3808 | 3809 | which-typed-array@1.1.15: 3810 | dependencies: 3811 | available-typed-arrays: 1.0.7 3812 | call-bind: 1.0.7 3813 | for-each: 0.3.3 3814 | gopd: 1.0.1 3815 | has-tostringtag: 1.0.2 3816 | 3817 | which@2.0.2: 3818 | dependencies: 3819 | isexe: 2.0.0 3820 | 3821 | word-wrap@1.2.5: {} 3822 | 3823 | wrap-ansi@7.0.0: 3824 | dependencies: 3825 | ansi-styles: 4.3.0 3826 | string-width: 4.2.3 3827 | strip-ansi: 6.0.1 3828 | 3829 | wrap-ansi@8.1.0: 3830 | dependencies: 3831 | ansi-styles: 6.2.1 3832 | string-width: 5.1.2 3833 | strip-ansi: 7.1.0 3834 | 3835 | wrappy@1.0.2: {} 3836 | 3837 | yocto-queue@0.1.0: {} 3838 | -------------------------------------------------------------------------------- /setup.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE user ( 2 | id INTEGER NOT NULL PRIMARY KEY, 3 | email TEXT NOT NULL UNIQUE, 4 | username TEXT NOT NULL, 5 | password_hash TEXT NOT NULL, 6 | email_verified INTEGER NOT NULL DEFAULT 0, 7 | totp_key BLOB, 8 | recovery_code BLOB NOT NULL 9 | ); 10 | 11 | CREATE INDEX email_index ON user(email); 12 | 13 | CREATE TABLE session ( 14 | id TEXT NOT NULL PRIMARY KEY, 15 | user_id INTEGER NOT NULL REFERENCES user(id), 16 | expires_at INTEGER NOT NULL, 17 | two_factor_verified INTEGER NOT NULL DEFAULT 0 18 | ); 19 | 20 | CREATE TABLE email_verification_request ( 21 | id TEXT NOT NULL PRIMARY KEY, 22 | user_id INTEGER NOT NULL REFERENCES user(id), 23 | email TEXT NOT NULL, 24 | code TEXT NOT NULL, 25 | expires_at INTEGER NOT NULL 26 | ); 27 | 28 | CREATE TABLE password_reset_session ( 29 | id TEXT NOT NULL PRIMARY KEY, 30 | user_id INTEGER NOT NULL REFERENCES user(id), 31 | email TEXT NOT NULL, 32 | code TEXT NOT NULL, 33 | expires_at INTEGER NOT NULL, 34 | email_verified INTEGER NOT NULL NOT NULL DEFAULT 0, 35 | two_factor_verified INTEGER NOT NULL DEFAULT 0 36 | ); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | }, 23 | "target": "ES2020" 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | --------------------------------------------------------------------------------