diff --git a/.vscode/settings.json b/.vscode/settings.json index baf98fae..4a1f28c2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -32,13 +32,20 @@ "Bloodmoon", "compat", "Embla", + "galar", + "gmax", + "hoenn", "ianvs", "jiti", + "johto", "jullerino", "JWKS", + "kalos", "nitropack", + "paldea", "Pressable", "prettiercache", + "sinnoh", "sluggable", "subrouters", "thatguyinabenaie", @@ -46,6 +53,7 @@ "tseslint", "typesafe", "unauthed", + "unova", "Ursaluna" ], "coverage-gutters.showGutterCoverage": false, diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index ea49290f..3b469198 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -55,6 +55,7 @@ "next": "catalog:", "openapi-fetch": "^0.13.0", "openapi-typescript-helpers": "^0.0.15", + "pokedex-promise-v2": "^4.2.0", "react": "catalog:react18", "react-dom": "catalog:react18", "server-only": "^0.0.1", diff --git a/apps/nextjs/src/app/api/cookies/user-id/route.ts b/apps/nextjs/src/app/api/cookies/user-id/route.ts new file mode 100644 index 00000000..23660f7f --- /dev/null +++ b/apps/nextjs/src/app/api/cookies/user-id/route.ts @@ -0,0 +1,63 @@ +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; +import { auth } from "@clerk/nextjs/server"; + +import { + generateSignature, + getCookie, + setResponseCookies, +} from "~/lib/cookies/cookies"; + +export const runtime = "edge"; + +export async function POST(_req: NextRequest) { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json( + { error: "Logged in account is required" }, + { status: 401 }, + ); + } + + const [response, setCookies] = setResponseCookies(); + const userIdCookie = await getCookie("userId"); + + if (userIdCookie) { + const [storedUserId, signature] = userIdCookie.split("."); + const expectedSignature = await generateSignature(storedUserId ?? ""); + + if (!storedUserId || !signature || signature !== expectedSignature) { + const msg = `Signature verification failed for userId cookie. Stored userId: ${storedUserId}, Expected signature: ${expectedSignature}`; + + console.warn(msg); // esl return await setUserIdCookie(setCookies, userId, response); + } + + const cookieExpiryDateValue = await getCookie("userId.expires"); + + if (!cookieExpiryDateValue) { + console.warn("Missing 'userId.expires' cookie."); // esl return await setUserIdCookie(setCookies, userId, response); + } + + const cookieExpiryDate = new Date(cookieExpiryDateValue); + + if (Number.isNaN(cookieExpiryDate.getTime())) { + console.warn("Invalid date in 'userId.expires' cookie."); // esl return await setUserIdCookie(setCookies, userId, response); + } + + if (cookieExpiryDate > new Date()) { + return response; + } + } + + return await setUserIdCookie(setCookies, userId, response); +} + +async function setUserIdCookie( + setCookies: (name: string, value: string) => Promise, + userId: string, + response: NextResponse, +) { + await setCookies("userId", userId); + + return response; +} diff --git a/apps/nextjs/src/app/api/discord/interactions/route.ts b/apps/nextjs/src/app/api/discord/interactions/route.ts new file mode 100644 index 00000000..0ba504e5 --- /dev/null +++ b/apps/nextjs/src/app/api/discord/interactions/route.ts @@ -0,0 +1,233 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unnecessary-condition */ +import type { + APIEmbed, + APIInteractionDataOptionBase, + ApplicationCommandOptionType, +} from "discord-api-types/v10"; +import type { NextRequest } from "next/server"; +import type Pokedex from "pokedex-promise-v2"; +import { NextResponse } from "next/server"; +import { auth } from "@clerk/nextjs/server"; +import { + InteractionResponseType, + InteractionType, + MessageFlags, +} from "discord-api-types/v10"; +import { nanoid } from "nanoid"; + +import type { RandomPicType } from "~/lib/discord/commands"; +import { env } from "~/env"; +import { commands } from "~/lib/discord/commands"; +import { verifyInteractionRequest } from "~/lib/discord/verify-incoming-request"; + +/** + * Use edge runtime which is faster, cheaper, and has no cold-boot. + * If you want to use node runtime, you can change this to `node`, but you'll also have to polyfill fetch (and maybe other things). + * + * @see https://nextjs.org/docs/app/building-your-application/rendering/edge-and-nodejs-runtimes + */ +export const runtime = "edge"; + +const ROOT_URL = env.VERCEL_URL ? `https://${env.VERCEL_URL}` : env.ROOT_URL; + +function capitalizeFirstLetter(s: string) { + return s.charAt(0).toUpperCase() + s.slice(1); +} + +/** + * Handle Discord interactions. Discord will send interactions to this endpoint. + * + * @see https://discord.com/developers/docs/interactions/receiving-and-responding#receiving-an-interaction + */ +export async function POST(request: NextRequest) { + const { userId } = await auth(); + if (!userId) { + return NextResponse.json( + { error: "Logged in account is required" }, + { status: 401 }, + ); + } + + if (!env.DISCORD_APP_PUBLIC_KEY || !env.DISCORD_APP_ID) { + return new NextResponse( + "DISCORD_APP_PUBLIC_KEY or DISCORD_APP_ID not initialized", + { status: 500 }, + ); + } + + const verifyResult = await verifyInteractionRequest( + request, + env.DISCORD_APP_PUBLIC_KEY, + ); + + if (!verifyResult.isValid || !verifyResult.interaction) { + return new NextResponse("Invalid request", { status: 401 }); + } + const { interaction } = verifyResult; + + if (interaction.type === InteractionType.Ping) { + // The `PING` message is used during the initial webhook handshake, and is + // required to configure the webhook in the developer portal. + return NextResponse.json({ type: InteractionResponseType.Pong }); + } + + if (interaction.type === InteractionType.ApplicationCommand) { + const { name } = interaction.data; + + switch (name) { + case commands.ping.name: { + return NextResponse.json({ + type: InteractionResponseType.ChannelMessageWithSource, + data: { content: `Pong` }, + }); + } + case commands.invite.name: { + return NextResponse.json({ + type: InteractionResponseType.ChannelMessageWithSource, + data: { + content: `Click this link to add NextBot to your server: https://discord.com/api/oauth2/authorize?client_id=${env.DISCORD_APP_ID}&permissions=2147485696&scope=bot%20applications.commands`, + flags: MessageFlags.Ephemeral, + }, + }); + } + case commands.pokemon.name: { + if (!interaction.data.options || interaction.data.options.length < 1) { + return NextResponse.json({ + type: InteractionResponseType.ChannelMessageWithSource, + data: { + content: "Oops! Please enter a Pokemon name or Pokedex number.", + flags: MessageFlags.Ephemeral, + }, + }); + } + + const option = interaction.data.options[0]; + + // @ts-expect-error copy pasta + const idOrName = String(option.value).toLowerCase(); + + try { + const pokemon: Pokedex.PokemonSpecies = await fetch( + `https://pokeapi.co/api/v2/pokemon/${idOrName}`, + ).then((res) => { + return res.json(); + }); + const types = pokemon.types.reduce( + (prev: string[], curr: { type: { name: string } }) => [ + ...prev, + capitalizeFirstLetter(curr.type.name), + ], + [], + ); + + return NextResponse.json({ + type: InteractionResponseType.ChannelMessageWithSource, + data: { + embeds: [ + { + title: capitalizeFirstLetter(pokemon.name), + image: { + url: `${ROOT_URL}/api/pokemon/${idOrName}`, + }, + fields: [ + { + name: "Pokedex", + value: `#${String(pokemon.id).padStart(3, "0")}`, + }, + { + name: "Type", + value: types.join("/"), + }, + ], + }, + ], + }, + }); + } catch (error) { + console.error("error fetching pokemon", error); + throw new Error("Something went wrong :("); + } + } + case commands.randompic.name: { + const { options } = interaction.data; + + if (!options) { + return new NextResponse("Invalid request", { status: 400 }); + } + + const { value } = options[0] as APIInteractionDataOptionBase< + ApplicationCommandOptionType.String, + RandomPicType + >; + const embed = await getRandomPic(value); + + return NextResponse.json({ + type: InteractionResponseType.ChannelMessageWithSource, + data: { embeds: [embed] }, + }); + } + default: + // Pass through, return error at end of function + } + } + + return new NextResponse("Unknown command", { status: 400 }); +} + +const baseRandomPicEmbed = { + title: "Random Pic", + description: "Here's your random pic!", +}; + +/** + * @see https://discord.com/developers/docs/resources/channel#embed-object + */ +const createEmbedObject = (source: string, path: string): APIEmbed => { + return { + ...baseRandomPicEmbed, + fields: [{ name: "source", value: source }], + image: { + url: `${source}${path}`, + }, + }; +}; + +/** + * Fetches a random picture and returns it as a Discord image embed. + */ +const getRandomPic = async (value: RandomPicType) => { + switch (value) { + case "cat": { + const catResponse = await fetch("https://cataas.com/cat?json=true"); + const catData = await catResponse.json(); + const { url: catUrl } = catData; + + return { + ...createEmbedObject("https://cataas.com", catUrl as string), + description: "Here's a random cat picture!", + }; + } + case "dog": { + const dogResponse = await fetch( + "https://dog.ceo/api/breeds/image/random", + ); + const dogData = await dogResponse.json(); + const { message: dogUrl } = dogData; + + return { + ...baseRandomPicEmbed, + description: "Here's a random dog picture!", + fields: [{ name: "source", value: "https://dog.ceo/api" }], + image: { url: dogUrl }, + }; + } + default: + return createEmbedObject( + "https://picsum.photos", + `/seed/${nanoid()}/500`, + ); + } +}; diff --git a/apps/nextjs/src/app/api/pokemon/pokepaste/route.ts b/apps/nextjs/src/app/api/pokemon/pokepaste/route.ts new file mode 100644 index 00000000..b33d0230 --- /dev/null +++ b/apps/nextjs/src/app/api/pokemon/pokepaste/route.ts @@ -0,0 +1,32 @@ +// TODO: fix restrict-template-expressions +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +// TODO: fix no unsafe assignment +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; + +export const runtime = "edge"; + +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + const { url } = body; + + if (!url || typeof url !== "string") { + return NextResponse.json( + { error: "Invalid URL format" }, + { status: 400 }, + ); + } + + const response = await fetch(url); + const text = await response.text(); + + return NextResponse.json({ data: text }, { status: 200 }); + } catch (error) { + return NextResponse.json( + { error: `Failed to fetch pokepaste ${error}` }, + { status: 500 }, + ); + } +} diff --git a/apps/nextjs/src/app/api/pokemon/team/validate/route.ts b/apps/nextjs/src/app/api/pokemon/team/validate/route.ts new file mode 100644 index 00000000..83fcc090 --- /dev/null +++ b/apps/nextjs/src/app/api/pokemon/team/validate/route.ts @@ -0,0 +1,196 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +// TODO: fix no unsafe assignment + +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; +import Pokedex from "pokedex-promise-v2"; + +import type { ParsedPokemon } from "~/lib/pokemon/common"; + +export const runtime = "edge"; + +function createPokedexInstance() { + try { + return new Pokedex(); + } catch (error) { + console.error("Error initializing Pokedex:", error); + throw new Error("Failed to initialize Pokedex"); + } +} + +const P = createPokedexInstance(); + +const validTypes = [ + "normal", + "fire", + "water", + "electric", + "grass", + "ice", + "fighting", + "poison", + "ground", + "flying", + "psychic", + "bug", + "rock", + "ghost", + "dragon", + "dark", + "steel", + "fairy", +]; + +const validTeraTypes = [...validTypes, "stellar"]; + +function toLowerCaseReplaceSpace(value: string) { + return value.toLowerCase().replace(/ /g, "-"); +} + +const species_modifiers = [ + "kanto", + "johto", + "hoenn", + "sinnoh", + "unova", + "kalos", + "alola", + "galar", + "paldea", + "gmax", + "blooddmoon", + "origin", + "female", + "male", + "mega", + "white", + "black", + "o", + "lele", + "koko", + "bulu", + "unbound", + "wellspring", + "hearthflame", + "cornerstone", + "terastal", + "stellar", + "droopy", + "curly", + "stretchy", +]; + +const special_cases: Record = { + maushold: "maushold-family-of-four", + tatsugiri: "tatsugiri-curly", +}; + +async function fetchPokemonData(pokemon: ParsedPokemon) { + try { + const lowerCaseSpecies = toLowerCaseReplaceSpace(pokemon.species); + + if (pokemon.species.includes("-")) { + if ( + species_modifiers.includes( + pokemon.species.split("-")[1]?.toLocaleLowerCase() ?? "", + ) + ) { + return await P.getPokemonByName(lowerCaseSpecies); + } + + return await P.getPokemonByName( + toLowerCaseReplaceSpace(pokemon.species.split("-")[0] ?? ""), + ); + } else { + const special_case_pokemon = special_cases[lowerCaseSpecies]; + + if (special_case_pokemon) { + return await P.getPokemonByName(special_case_pokemon); + } + + return await P.getPokemonByName(lowerCaseSpecies); + } + } catch (error) { + const message = `Error fetching Pokemon data for ${pokemon.species}: ${error}`; + console.error(message); + return NextResponse.json({ message }, { status: 400 }); + } +} + +export async function POST(req: NextRequest) { + const { pokemon } = (await req.json()) as { pokemon: ParsedPokemon }; + + const pokemonData = await fetchPokemonData(pokemon); + + if (pokemonData instanceof NextResponse) { + return pokemonData; + } + + try { + const validItem = pokemon.item + ? await P.getItemByName(toLowerCaseReplaceSpace(pokemon.item)) + : null; + + const lowercaseAbility = toLowerCaseReplaceSpace(pokemon.ability); + + const validAbility = pokemonData.abilities.find( + ({ ability }) => ability.name === lowercaseAbility, + ); + const invalidMoves = pokemon.moves + .map(toLowerCaseReplaceSpace) + .filter( + (pokemon_move: string) => + !pokemonData.moves.find(({ move }) => move.name === pokemon_move), + ) + .map((move) => + move.replace(/-/g, " ").replace(/\b\w/g, (char) => char.toUpperCase()), + ); + + const validTeraType = validTeraTypes.includes( + pokemon.teraType?.toLowerCase() ?? "", + ); + + if (!validTeraType) { + return NextResponse.json( + { message: "Invalid Tera Type" }, + { status: 400 }, + ); + } + + const invalid = { + item: !validItem, + ability: !validAbility, + moves: invalidMoves, + teraType: !validTeraType, + }; + + const officialArtwork = pokemonData.sprites.other["official-artwork"]; + + const parsedPokemon = { + ...pokemon, + imgPokemon: pokemon.shiny + ? officialArtwork.front_shiny + : officialArtwork.front_default, + imgItem: validItem?.sprites.default, + }; + + // if (pokemon.form) { + // const formName = `${pokemon.baseSpecies}-${pokemon.form}`.toLowerCase(); + // const formData = await P.getPokemonFormByName(formName); + // if (formData.sprites.front_default) { + // parsedPokemon.imgPokemon = formData.sprites.front_default; + // } + // } + + return NextResponse.json( + { pokemon: parsedPokemon, invalid }, + { status: 200 }, + ); + } catch (error) { + console.error("Error fetching Pokemon data", error); + return NextResponse.json( + { message: `Error fetching Pokemon data}` }, + { status: 500 }, + ); + } +} diff --git a/apps/nextjs/src/env.ts b/apps/nextjs/src/env.ts index a1f2d840..7d587a16 100644 --- a/apps/nextjs/src/env.ts +++ b/apps/nextjs/src/env.ts @@ -24,6 +24,31 @@ export const env = createEnv({ WEBSOCKET_URL: z.string().optional(), MEASUREMENT_ID: z.string().default("G-XXXXXXXXXX"), UPLOADTHING_SECRET: z.string(), + ROOT_URL: z + .string() + .url("ROOT_URL must be a valid URL") + .optional() + .default("http://localhost:10000"), + DISCORD_APP_PUBLIC_KEY: z + .string({ + required_error: + "DISCORD_APP_PUBLIC_KEY is required. Visit https://discord.com/developers/applications -> General information -> PUBLIC KEY", + }) + .min( + 1, + "DISCORD_APP_PUBLIC_KEY is required. Visit https://discord.com/developers/applications -> General information -> PUBLIC KEY", + ) + .optional(), + DISCORD_APP_ID: z + .string({ + required_error: + "DISCORD_APP_ID is required. Visit https://discord.com/developers/applications -> Your bot -> General information -> Application ID", + }) + .min( + 1, + "DISCORD_APP_ID is required. Visit https://discord.com/developers/applications -> Your bot -> General information -> Application ID", + ) + .optional(), }, /** diff --git a/apps/nextjs/src/lib/cookies/cookies.ts b/apps/nextjs/src/lib/cookies/cookies.ts new file mode 100644 index 00000000..56590a71 --- /dev/null +++ b/apps/nextjs/src/lib/cookies/cookies.ts @@ -0,0 +1,75 @@ +import { headers } from "next/headers"; +import { NextResponse } from "next/server"; +import * as cookie from "cookie"; + +import { env } from "~/env"; + +function getCookieDomain() { + if (env.NODE_ENV === "production") { + return env.COOKIE_DOMAIN; + } + + return "localhost"; +} + +const maxAge = 60 * 60 * 24; // 1 day in seconds +const defaultCookieOptions: cookie.SerializeOptions = { + httpOnly: true, + secure: env.NODE_ENV === "production", + sameSite: "none", + domain: getCookieDomain(), + path: "/", + maxAge, +}; + +export function setResponseCookies() { + const response = NextResponse.json({ message: "Cookie set successfully" }); + + async function setCookies( + key: string, + value: string | number, + ): Promise { + const encodedValue = encodeURIComponent(`${value}`).replace(/\./g, "%2E"); + const signedEncodedValue = `${encodedValue}.${await generateSignature(encodedValue)}`; + + response.headers.set( + "Set-Cookie", + cookie.serialize(key, signedEncodedValue, defaultCookieOptions), + ); + + const expires = new Date(Date.now() + maxAge * 1000).toUTCString(); + + response.headers.append( + "Set-Cookie", + cookie.serialize(`${key}.expires`, expires, defaultCookieOptions), + ); + } + + return [response, setCookies] as const; +} + +export async function generateSignature( + value: string | number, +): Promise { + const encoder = new TextEncoder(); + const keyData = encoder.encode(env.AUTH_SECRET); + const valueData = encoder.encode(`${value}`); + + const key = await crypto.subtle.importKey( + "raw", + keyData, + { name: "HMAC", hash: { name: "SHA-256" } }, + false, + ["sign"], + ); + + const signature = await crypto.subtle.sign("HMAC", key, valueData); + + return Array.from(new Uint8Array(signature)) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); +} + +export async function getCookie(cookie: string) { + return (await headers()).get(cookie) ?? ""; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0a162ff3..1913c5d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -117,7 +117,7 @@ importers: version: 1.13.0 nitropack: specifier: ^2.9.7 - version: 2.10.3(@planetscale/database@1.19.0)(@upstash/redis@1.34.3)(drizzle-orm@0.36.1(@planetscale/database@1.19.0)(mysql2@3.11.3))(mysql2@3.11.3)(typescript@5.6.3)(webpack-sources@3.2.3) + version: 2.10.3(@planetscale/database@1.19.0)(@upstash/redis@1.34.3)(drizzle-orm@0.36.1(@libsql/client-wasm@0.14.0)(@neondatabase/serverless@0.10.3)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/pg@8.11.10)(@types/react@18.3.11)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(mysql2@3.11.3)(postgres@3.4.4)(react@18.3.1))(mysql2@3.11.3)(typescript@5.6.3)(webpack-sources@3.2.3) prettier: specifier: 'catalog:' version: 3.3.3 @@ -362,6 +362,9 @@ importers: openapi-typescript-helpers: specifier: ^0.0.15 version: 0.0.15 + pokedex-promise-v2: + specifier: ^4.2.0 + version: 4.2.0 react: specifier: catalog:react18 version: 18.3.1 @@ -535,13 +538,13 @@ importers: version: 0.10.3 drizzle-orm: specifier: ^0.35.1 - version: 0.35.3(@libsql/client-wasm@0.14.0)(@neondatabase/serverless@0.10.3)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/pg@8.11.10)(@types/react@18.3.11)(@vercel/postgres@0.10.0)(mysql2@3.11.3)(postgres@3.4.4)(react@18.3.1) + version: 0.35.3(@libsql/client-wasm@0.14.0)(@neondatabase/serverless@0.10.3)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/pg@8.11.10)(@types/react@18.3.11)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(mysql2@3.11.3)(postgres@3.4.4)(react@18.3.1) drizzle-typebox: specifier: ^0.1.1 - version: 0.1.1(@sinclair/typebox@0.27.8)(drizzle-orm@0.35.3(@libsql/client-wasm@0.14.0)(@neondatabase/serverless@0.10.3)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/pg@8.11.10)(@types/react@18.3.11)(@vercel/postgres@0.10.0)(mysql2@3.11.3)(postgres@3.4.4)(react@18.3.1)) + version: 0.1.1(@sinclair/typebox@0.27.8)(drizzle-orm@0.35.3(@libsql/client-wasm@0.14.0)(@neondatabase/serverless@0.10.3)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/pg@8.11.10)(@types/react@18.3.11)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(mysql2@3.11.3)(postgres@3.4.4)(react@18.3.1)) drizzle-zod: specifier: ^0.5.1 - version: 0.5.1(drizzle-orm@0.35.3(@libsql/client-wasm@0.14.0)(@neondatabase/serverless@0.10.3)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/pg@8.11.10)(@types/react@18.3.11)(@vercel/postgres@0.10.0)(mysql2@3.11.3)(postgres@3.4.4)(react@18.3.1))(zod@3.23.8) + version: 0.5.1(drizzle-orm@0.35.3(@libsql/client-wasm@0.14.0)(@neondatabase/serverless@0.10.3)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/pg@8.11.10)(@types/react@18.3.11)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(mysql2@3.11.3)(postgres@3.4.4)(react@18.3.1))(zod@3.23.8) zod: specifier: 'catalog:' version: 3.23.8 @@ -4682,6 +4685,9 @@ packages: resolution: {integrity: sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g==} engines: {node: '>=4'} + axios@1.7.7: + resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -5000,6 +5006,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + clsx@2.0.0: resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==} engines: {node: '>=6'} @@ -6226,6 +6236,15 @@ packages: resolution: {integrity: sha512-fkCfVPelbTzSVp+jVwSvEyc+I4WG8MNhRG/EWSZZTlgHAMEdhXJaFEbfErXxMktboMhVGchvEFhWxkzNGM1m2A==} engines: {node: '>=0.4.0'} + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + fontfaceobserver@2.3.0: resolution: {integrity: sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==} @@ -6240,6 +6259,10 @@ packages: resolution: {integrity: sha512-sJe+TQb2vIaIyO783qN6BlMYWMw3WBOHA1Ay2qxsnjuafEOQFJ2JakedOQirT6D5XPRxDvS7AHYyem9fTpb4LQ==} engines: {node: '>= 6'} + form-data@4.0.1: + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + engines: {node: '>= 6'} + freeport-async@2.0.0: resolution: {integrity: sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ==} engines: {node: '>=8'} @@ -7660,6 +7683,10 @@ packages: node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-cache@5.1.2: + resolution: {integrity: sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==} + engines: {node: '>= 8.0.0'} + node-dir@0.1.17: resolution: {integrity: sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==} engines: {node: '>= 0.10.5'} @@ -7889,6 +7916,10 @@ packages: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} engines: {node: '>=10'} + p-map@7.0.2: + resolution: {integrity: sha512-z4cYYMMdKHzw4O5UkWJImbZynVIo0lSGTXc7bzB1e/rrDqkgGUNysK/o4bTr+0+xKvvLoTyGqYC4Fgljy9qe1Q==} + engines: {node: '>=18'} + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -8048,6 +8079,10 @@ packages: resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==} engines: {node: '>=4.0.0'} + pokedex-promise-v2@4.2.0: + resolution: {integrity: sha512-7BnpWbq/aIMTDLsy0ciNRgkP5NGLQ2OcKI0QuePNdxbB1ZAmjxNZ1D7x6sfVgZwMFM+eewrr0eeStCzZ0XwiWw==} + engines: {node: '>=18'} + possible-typed-array-names@1.0.0: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} @@ -11816,7 +11851,7 @@ snapshots: dependencies: '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.9.0 + '@types/node': 20.17.6 '@types/yargs': 15.0.19 chalk: 4.1.2 optional: true @@ -13798,7 +13833,7 @@ snapshots: '@types/pg@8.11.10': dependencies: - '@types/node': 22.9.0 + '@types/node': 20.17.6 pg-protocol: 1.7.0 pg-types: 4.0.2 optional: true @@ -13992,7 +14027,7 @@ snapshots: - encoding - supports-color - '@vercel/postgres@0.10.0': + '@vercel/postgres@0.10.0(utf-8-validate@6.0.4)': dependencies: '@neondatabase/serverless': 0.9.5 bufferutil: 4.0.8 @@ -14270,6 +14305,14 @@ snapshots: axe-core@4.10.0: {} + axios@1.7.7: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.1 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axobject-query@4.1.0: {} b4a@1.6.7: {} @@ -14691,6 +14734,8 @@ snapshots: clone@1.0.4: {} + clone@2.1.2: {} + clsx@2.0.0: {} cluster-key-slot@1.1.2: {} @@ -14951,9 +14996,9 @@ snapshots: date-fns@4.1.0: {} - db0@0.2.1(drizzle-orm@0.36.1(@planetscale/database@1.19.0)(mysql2@3.11.3))(mysql2@3.11.3): + db0@0.2.1(drizzle-orm@0.36.1(@libsql/client-wasm@0.14.0)(@neondatabase/serverless@0.10.3)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/pg@8.11.10)(@types/react@18.3.11)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(mysql2@3.11.3)(postgres@3.4.4)(react@18.3.1))(mysql2@3.11.3): optionalDependencies: - drizzle-orm: 0.36.1(@planetscale/database@1.19.0)(mysql2@3.11.3) + drizzle-orm: 0.36.1(@libsql/client-wasm@0.14.0)(@neondatabase/serverless@0.10.3)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/pg@8.11.10)(@types/react@18.3.11)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(mysql2@3.11.3)(postgres@3.4.4)(react@18.3.1) mysql2: 3.11.3 debug@2.6.9: @@ -15128,7 +15173,7 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.35.3(@libsql/client-wasm@0.14.0)(@neondatabase/serverless@0.10.3)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/pg@8.11.10)(@types/react@18.3.11)(@vercel/postgres@0.10.0)(mysql2@3.11.3)(postgres@3.4.4)(react@18.3.1): + drizzle-orm@0.35.3(@libsql/client-wasm@0.14.0)(@neondatabase/serverless@0.10.3)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/pg@8.11.10)(@types/react@18.3.11)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(mysql2@3.11.3)(postgres@3.4.4)(react@18.3.1): dependencies: '@libsql/client-wasm': 0.14.0 optionalDependencies: @@ -15137,25 +15182,33 @@ snapshots: '@planetscale/database': 1.19.0 '@types/pg': 8.11.10 '@types/react': 18.3.11 - '@vercel/postgres': 0.10.0 + '@vercel/postgres': 0.10.0(utf-8-validate@6.0.4) mysql2: 3.11.3 postgres: 3.4.4 react: 18.3.1 - drizzle-orm@0.36.1(@planetscale/database@1.19.0)(mysql2@3.11.3): + drizzle-orm@0.36.1(@libsql/client-wasm@0.14.0)(@neondatabase/serverless@0.10.3)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/pg@8.11.10)(@types/react@18.3.11)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(mysql2@3.11.3)(postgres@3.4.4)(react@18.3.1): optionalDependencies: + '@libsql/client-wasm': 0.14.0 + '@neondatabase/serverless': 0.10.3 + '@opentelemetry/api': 1.9.0 '@planetscale/database': 1.19.0 + '@types/pg': 8.11.10 + '@types/react': 18.3.11 + '@vercel/postgres': 0.10.0(utf-8-validate@6.0.4) mysql2: 3.11.3 + postgres: 3.4.4 + react: 18.3.1 optional: true - drizzle-typebox@0.1.1(@sinclair/typebox@0.27.8)(drizzle-orm@0.35.3(@libsql/client-wasm@0.14.0)(@neondatabase/serverless@0.10.3)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/pg@8.11.10)(@types/react@18.3.11)(@vercel/postgres@0.10.0)(mysql2@3.11.3)(postgres@3.4.4)(react@18.3.1)): + drizzle-typebox@0.1.1(@sinclair/typebox@0.27.8)(drizzle-orm@0.35.3(@libsql/client-wasm@0.14.0)(@neondatabase/serverless@0.10.3)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/pg@8.11.10)(@types/react@18.3.11)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(mysql2@3.11.3)(postgres@3.4.4)(react@18.3.1)): dependencies: '@sinclair/typebox': 0.27.8 - drizzle-orm: 0.35.3(@libsql/client-wasm@0.14.0)(@neondatabase/serverless@0.10.3)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/pg@8.11.10)(@types/react@18.3.11)(@vercel/postgres@0.10.0)(mysql2@3.11.3)(postgres@3.4.4)(react@18.3.1) + drizzle-orm: 0.35.3(@libsql/client-wasm@0.14.0)(@neondatabase/serverless@0.10.3)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/pg@8.11.10)(@types/react@18.3.11)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(mysql2@3.11.3)(postgres@3.4.4)(react@18.3.1) - drizzle-zod@0.5.1(drizzle-orm@0.35.3(@libsql/client-wasm@0.14.0)(@neondatabase/serverless@0.10.3)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/pg@8.11.10)(@types/react@18.3.11)(@vercel/postgres@0.10.0)(mysql2@3.11.3)(postgres@3.4.4)(react@18.3.1))(zod@3.23.8): + drizzle-zod@0.5.1(drizzle-orm@0.35.3(@libsql/client-wasm@0.14.0)(@neondatabase/serverless@0.10.3)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/pg@8.11.10)(@types/react@18.3.11)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(mysql2@3.11.3)(postgres@3.4.4)(react@18.3.1))(zod@3.23.8): dependencies: - drizzle-orm: 0.35.3(@libsql/client-wasm@0.14.0)(@neondatabase/serverless@0.10.3)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/pg@8.11.10)(@types/react@18.3.11)(@vercel/postgres@0.10.0)(mysql2@3.11.3)(postgres@3.4.4)(react@18.3.1) + drizzle-orm: 0.35.3(@libsql/client-wasm@0.14.0)(@neondatabase/serverless@0.10.3)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/pg@8.11.10)(@types/react@18.3.11)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(mysql2@3.11.3)(postgres@3.4.4)(react@18.3.1) zod: 3.23.8 duplexer@0.1.2: {} @@ -16006,6 +16059,8 @@ snapshots: flow-parser@0.248.1: {} + follow-redirects@1.15.9: {} + fontfaceobserver@2.3.0: {} for-each@0.3.3: @@ -16023,6 +16078,12 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 + form-data@4.0.1: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + freeport-async@2.0.0: {} fresh@0.5.2: {} @@ -17546,7 +17607,7 @@ snapshots: nice-try@1.0.5: {} - nitropack@2.10.3(@planetscale/database@1.19.0)(@upstash/redis@1.34.3)(drizzle-orm@0.36.1(@planetscale/database@1.19.0)(mysql2@3.11.3))(mysql2@3.11.3)(typescript@5.6.3)(webpack-sources@3.2.3): + nitropack@2.10.3(@planetscale/database@1.19.0)(@upstash/redis@1.34.3)(drizzle-orm@0.36.1(@libsql/client-wasm@0.14.0)(@neondatabase/serverless@0.10.3)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/pg@8.11.10)(@types/react@18.3.11)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(mysql2@3.11.3)(postgres@3.4.4)(react@18.3.1))(mysql2@3.11.3)(typescript@5.6.3)(webpack-sources@3.2.3): dependencies: '@cloudflare/kv-asset-handler': 0.3.4 '@netlify/functions': 2.8.2 @@ -17570,7 +17631,7 @@ snapshots: cookie-es: 1.2.2 croner: 9.0.0 crossws: 0.3.1 - db0: 0.2.1(drizzle-orm@0.36.1(@planetscale/database@1.19.0)(mysql2@3.11.3))(mysql2@3.11.3) + db0: 0.2.1(drizzle-orm@0.36.1(@libsql/client-wasm@0.14.0)(@neondatabase/serverless@0.10.3)(@opentelemetry/api@1.9.0)(@planetscale/database@1.19.0)(@types/pg@8.11.10)(@types/react@18.3.11)(@vercel/postgres@0.10.0(utf-8-validate@6.0.4))(mysql2@3.11.3)(postgres@3.4.4)(react@18.3.1))(mysql2@3.11.3) defu: 6.1.4 destr: 2.0.3 dot-prop: 9.0.0 @@ -17655,6 +17716,10 @@ snapshots: node-addon-api@7.1.1: {} + node-cache@5.1.2: + dependencies: + clone: 2.1.2 + node-dir@0.1.17: dependencies: minimatch: 3.1.2 @@ -17921,6 +17986,8 @@ snapshots: dependencies: aggregate-error: 3.1.0 + p-map@7.0.2: {} + p-try@2.2.0: {} pac-proxy-agent@7.0.2: @@ -18063,6 +18130,14 @@ snapshots: pngjs@3.4.0: {} + pokedex-promise-v2@4.2.0: + dependencies: + axios: 1.7.7 + node-cache: 5.1.2 + p-map: 7.0.2 + transitivePeerDependencies: + - debug + possible-typed-array-names@1.0.0: {} postcss-import@15.1.0(postcss@8.4.47):