From a42bc6217e8f3491211fa4f8db1c6de84f6ee6c3 Mon Sep 17 00:00:00 2001 From: Carlos Date: Sun, 13 Sep 2026 14:15:03 -0400 Subject: [PATCH 001/135] feat: load max-level enchantments from backend --- src/lib/shared/api/orval-generated-zod.ts | 8 + src/lib/shared/api/orval-generated.ts | 29 ++++ src/lib/shared/api/skycrypt-api.remote.ts | 6 + src/lib/shared/constants/enchantments.ts | 141 ++---------------- src/lib/shared/mc-text/mc-text.svelte.spec.ts | 3 + src/lib/types/index.ts | 1 + src/routes/+layout.server.ts | 15 ++ src/routes/+layout.svelte | 7 +- 8 files changed, 78 insertions(+), 132 deletions(-) create mode 100644 src/routes/+layout.server.ts diff --git a/src/lib/shared/api/orval-generated-zod.ts b/src/lib/shared/api/orval-generated-zod.ts index de0cd028b..63e414f2f 100644 --- a/src/lib/shared/api/orval-generated-zod.ts +++ b/src/lib/shared/api/orval-generated-zod.ts @@ -2647,6 +2647,14 @@ export const GetCombinedProfileStatsResponse = zod.object({ .optional() }); +/** + * Returns the lore strings for enchantments at their maximum level. + * + * @summary List max-level enchantments + */ +export const ListEnchantmentsResponseItem = zod.string(); +export const ListEnchantmentsResponse = zod.array(ListEnchantmentsResponseItem); + /** * Returns toggleable resource packs sorted by descending priority for the recommended default order. The enabledPacks * cookie controls per-request rendering from highest to lowest priority and does not change this response order. A diff --git a/src/lib/shared/api/orval-generated.ts b/src/lib/shared/api/orval-generated.ts index 400622230..42b1ccd49 100644 --- a/src/lib/shared/api/orval-generated.ts +++ b/src/lib/shared/api/orval-generated.ts @@ -1399,6 +1399,35 @@ export const getCombinedProfileStats = async ( }); }; +export type listEnchantmentsResponse200 = { + data: string[]; + status: 200; +}; + +export type listEnchantmentsResponseSuccess = listEnchantmentsResponse200 & { + headers: Headers; +}; + +export type listEnchantmentsResponse = listEnchantmentsResponseSuccess; + +export const getListEnchantmentsUrl = () => { + return `/api/constants/enchantments`; +}; + +/** + * Returns the lore strings for enchantments at their maximum level. + * + * @summary List max-level enchantments + */ +export const listEnchantments = async ( + options?: Parameters[1] +): Promise => { + return customFetch(getListEnchantmentsUrl(), { + ...options, + method: "GET" + }); +}; + export type listResourcePacksResponse200 = { data: ModelsResourcePackConfig[]; status: 200; diff --git a/src/lib/shared/api/skycrypt-api.remote.ts b/src/lib/shared/api/skycrypt-api.remote.ts index fc9d69f2c..ca3dbd95b 100644 --- a/src/lib/shared/api/skycrypt-api.remote.ts +++ b/src/lib/shared/api/skycrypt-api.remote.ts @@ -10,6 +10,7 @@ import { getSelectedProfileEmbed as getSelectedProfileEmbedRequest, getSelectedProfileStats as getSelectedProfileStatsRequest, getSourceInfo as getSourceInfoRequest, + listEnchantments as listEnchantmentsRequest, listResourcePacks as listResourcePacksRequest, resolveUsernameByUuid as resolveUsernameByUuidRequest, resolveUuidByUsername as resolveUuidByUsernameRequest, @@ -141,6 +142,11 @@ export const getAllStats = prerender(async () => { return fetchSection(APIEndpointName.STATS, () => getStatsConstantsRequest()); }); +/** Fetch max-level enchantment lore strings */ +export const getEnchantments = prerender(async () => { + return fetchSection(APIEndpointName.ENCHANTMENTS, () => listEnchantmentsRequest()); +}); + /** Fetch the source information for the currently running backend service. */ export const getSourceInfo = query(async (): Promise => { try { diff --git a/src/lib/shared/constants/enchantments.ts b/src/lib/shared/constants/enchantments.ts index bebdbd7a2..65bb5364e 100644 --- a/src/lib/shared/constants/enchantments.ts +++ b/src/lib/shared/constants/enchantments.ts @@ -1,131 +1,10 @@ -export const MAX_ENCHANTS = new Set([ - "Absorb X", - "Angler VI", - "Aqua Affinity I", - "Bane of Arthropods VII", - "Big Brain V", - "Blast Protection VII", - "Blessing VI", - "Caster VI", - "Cayenne V", - "Champion X", - "Chance V", - "Charm V", - "Cleave VI", - "Compact X", - "Corruption V", - "Counter-Strike V", - "Critical VII", - "Cubism VI", - "Cultivating X", - "Dedication IV", - "Delicate V", - "Depth Strider III", - "Divine Gift III", - "Dragon Tracer V", - "Drain V", - "Efficiency VI", - "Efficiency X", - "Ender Slayer VII", - "Execute VI", - "Experience V", - "Expertise X", - "Feather Falling X", - "Feather Falling XX", - "Ferocious Mana X", - "Fire Aspect III", - "Fire Protection VII", - "First Strike V", - "Flame II", - "Forest Pledge VI", - "Fortune IV", - "Frail VI", - "Giant Killer VII", - "Gravity VI", - "Green Thumb V", - "Growth VII", - "Hardened Mana X", - "Harvesting VI", - "Hecatomb X", - "Ice Cold V", - "Impaling V", - "Infinite Quiver X", - "Karma VI", - "Knockback II", - "Lapidary V", - "Lethality VI", - "Life Steal V", - "Looting V", - "Luck VII", - "Luck of the Sea VII", - "Lure VI", - "Magnet VI", - "Mana Steal III", - "Mana Vampire X", - "Overload V", - "Paleontologist V", - "Pesterminator VI", - "Petalfall V", - "Piercing I", - "Piscary VI", - "Power VII", - "Prismatic V", - "Projectile Protection VII", - "Prosecute VI", - "Prosperity V", - "Protection VII", - "Punch II", - "Pyroclasm VI", - "Quantum V", - "Quick Bite V", - "Rainbow I", - "Rainbow II", - "Rainbow III", - "Reflection V", - "Rejuvenate V", - "Replenish I", - "Respiration IV", - "Respite V", - "Scavenger VI", - "Scuba VI", - "Sharpness VII", - "Silk Touch I", - "Small Brain V", - "Smarty Pants V", - "Smelting Touch I", - "Smite VII", - "Smoldering V", - "Snipe IV", - "Spiked Hook VI", - "Stealth I", - "Strong Mana X", - "Sugar Rush III", - "Sunder VI", - "Tabasco III", - "Thorns IV", - "Thunderbolt VI", - "Thunderlord VII", - "Tidal III", - "Titan Killer VII", - "Toxophilite X", - "Transylvanian V", - "Triple-Strike V", - "True Protection I", - "Turbo-Cacti VII", - "Turbo-Cane VII", - "Turbo-Carrot VII", - "Turbo-Cocoa VII", - "Turbo-Melon VII", - "Turbo-Moonflower VII", - "Turbo-Mushrooms VII", - "Turbo-Potato VII", - "Turbo-Pumpkin VII", - "Turbo-Rose VII", - "Turbo-Sunflower VII", - "Turbo-Warts VII", - "Turbo-Wheat VII", - "Vampirism VI", - "Venomous VII", - "Vicious V", - "Woodsplitter VI" -]); +/** Max-level enchantment lore strings returned by the backend constants endpoint. */ +export const MAX_ENCHANTS = new Set(); + +/** Replace the runtime enchantment set with the backend-owned list. */ +export function setMaxEnchantments(enchantments: readonly string[]): void { + MAX_ENCHANTS.clear(); + for (const enchantment of enchantments) { + MAX_ENCHANTS.add(enchantment); + } +} diff --git a/src/lib/shared/mc-text/mc-text.svelte.spec.ts b/src/lib/shared/mc-text/mc-text.svelte.spec.ts index 008f63dc0..277dc866a 100644 --- a/src/lib/shared/mc-text/mc-text.svelte.spec.ts +++ b/src/lib/shared/mc-text/mc-text.svelte.spec.ts @@ -1,7 +1,10 @@ +import { setMaxEnchantments } from "$lib/shared/constants/enchantments"; import { mcTextToHTML } from "$lib/shared/mc-text"; import { colorCodes, extras } from "$lib/shared/mc-text/parser/styleLibrary"; import { describe, it } from "vitest"; +setMaxEnchantments(["Aqua Affinity I"]); + function sortedClasses(element: Element): string[] { return Array.from(element.classList).sort(); } diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts index 2cb8d8118..eba5092a2 100644 --- a/src/lib/types/index.ts +++ b/src/lib/types/index.ts @@ -20,6 +20,7 @@ export enum APIEndpointName { GARDEN = "Garden", EMBED = "Embed Data", RESOURCEPACK = "Resource Pack", + ENCHANTMENTS = "Enchantments", SEARCH = "Search", USERNAME = "Username" } diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts new file mode 100644 index 000000000..201ffc171 --- /dev/null +++ b/src/routes/+layout.server.ts @@ -0,0 +1,15 @@ +import { getEnchantments } from "$lib/shared/api/skycrypt-api.remote"; +import { setMaxEnchantments } from "$lib/shared/constants/enchantments"; +import type { LayoutServerLoad } from "./$types"; + +export const load = (async () => { + try { + const maxEnchantments = await getEnchantments(); + setMaxEnchantments(maxEnchantments); + return { maxEnchantments }; + } catch (error) { + console.error("Failed to load max-level enchantments", error); + setMaxEnchantments([]); + return { maxEnchantments: [] }; + } +}) satisfies LayoutServerLoad; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 983bc6501..382fa048c 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -25,6 +25,7 @@ import { IsMobile } from "$lib/hooks/is-mobile.svelte"; import { listLatestPostsForNotifications } from "$lib/shared/api/cms-api.remote"; import { listResourcePacks } from "$lib/shared/api/skycrypt-api.remote"; + import { setMaxEnchantments } from "$lib/shared/constants/enchantments"; import { parseThemeFromURL } from "$lib/shared/themes/sharing"; import * as Drawer from "$ui/drawer"; import { Separator } from "$ui/separator"; @@ -39,8 +40,12 @@ import "./layout.css"; import { SvelteURLSearchParams } from "svelte/reactivity"; import { writable } from "svelte/store"; + import type { LayoutData } from "./$types"; - let { children }: { children: Snippet } = $props(); + let { children, data }: { children: Snippet; data: LayoutData } = $props(); + $effect.pre(() => { + setMaxEnchantments(data.maxEnchantments); + }); let isMobile = $state(new IsMobile()); let isHover = $state(new IsHover()); let toastId: string | number = $state(0); From 22b69c9714324955032928be83287ba66390a3bc Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 14 Sep 2026 20:42:20 -0400 Subject: [PATCH 002/135] refactor: fixing reviews --- .changeset/tidy-enchantments-api.md | 5 +++++ src/context/createContext.svelte.ts | 13 ++++++++++++ src/lib/shared/api/skycrypt-api.remote.ts | 2 +- src/lib/shared/constants/enchantments.ts | 12 ++++------- src/lib/shared/helper.svelte.spec.ts | 4 ++++ src/lib/shared/mc-text/mc-text.svelte.spec.ts | 7 ++++--- src/lib/shared/mc-text/parser/mcTextToHTML.ts | 4 ++-- src/routes/+layout.server.ts | 15 -------------- src/routes/+layout.svelte | 20 ++++++++++++------- 9 files changed, 46 insertions(+), 36 deletions(-) create mode 100644 .changeset/tidy-enchantments-api.md delete mode 100644 src/routes/+layout.server.ts diff --git a/.changeset/tidy-enchantments-api.md b/.changeset/tidy-enchantments-api.md new file mode 100644 index 000000000..595bbd465 --- /dev/null +++ b/.changeset/tidy-enchantments-api.md @@ -0,0 +1,5 @@ +--- +"skycrypt-frontend": patch +--- + +Load max-level enchantments from the backend constants API. diff --git a/src/context/createContext.svelte.ts b/src/context/createContext.svelte.ts index a9ad8fb1d..09a349845 100644 --- a/src/context/createContext.svelte.ts +++ b/src/context/createContext.svelte.ts @@ -34,6 +34,18 @@ export class AllStatsContext { } } +export class EnchantmentsContext { + #current: string[] = $state([]); + + get current() { + return this.#current; + } + + set current(value: string[]) { + this.#current = value; + } +} + export class CombinedContext { #current: ModelsCombinedOutput | null = $state(null); @@ -84,6 +96,7 @@ export class SkillsContext { export const [getProfileContext, setProfileContext] = createContext(); export const [getAllStatsContext, setAllStatsContext] = createContext(); +export const [getEnchantmentsContext, setEnchantmentsContext] = createContext(); export const [getCombinedContext, setCombinedContext] = createContext(); export const [getSkillsContext, setSkillsContext] = createContext(); export const [getMiscContext, setMiscContext] = createContext(); diff --git a/src/lib/shared/api/skycrypt-api.remote.ts b/src/lib/shared/api/skycrypt-api.remote.ts index ca3dbd95b..5ff17092e 100644 --- a/src/lib/shared/api/skycrypt-api.remote.ts +++ b/src/lib/shared/api/skycrypt-api.remote.ts @@ -143,7 +143,7 @@ export const getAllStats = prerender(async () => { }); /** Fetch max-level enchantment lore strings */ -export const getEnchantments = prerender(async () => { +export const listEnchantments = prerender(async () => { return fetchSection(APIEndpointName.ENCHANTMENTS, () => listEnchantmentsRequest()); }); diff --git a/src/lib/shared/constants/enchantments.ts b/src/lib/shared/constants/enchantments.ts index 65bb5364e..aa75f7cd8 100644 --- a/src/lib/shared/constants/enchantments.ts +++ b/src/lib/shared/constants/enchantments.ts @@ -1,10 +1,6 @@ -/** Max-level enchantment lore strings returned by the backend constants endpoint. */ -export const MAX_ENCHANTS = new Set(); +import { getEnchantmentsContext } from "$ctx"; -/** Replace the runtime enchantment set with the backend-owned list. */ -export function setMaxEnchantments(enchantments: readonly string[]): void { - MAX_ENCHANTS.clear(); - for (const enchantment of enchantments) { - MAX_ENCHANTS.add(enchantment); - } +/** Check whether an enchantment is at its maximum level according to the backend constants. */ +export function isMaxEnchantment(enchantment: string): boolean { + return getEnchantmentsContext().current.includes(enchantment); } diff --git a/src/lib/shared/helper.svelte.spec.ts b/src/lib/shared/helper.svelte.spec.ts index a38940c60..90856442d 100644 --- a/src/lib/shared/helper.svelte.spec.ts +++ b/src/lib/shared/helper.svelte.spec.ts @@ -1,6 +1,10 @@ import { renderLore } from "$lib/shared/helper"; import { afterEach, beforeEach, describe, it, vi } from "vitest"; +vi.mock("$ctx", () => ({ + getEnchantmentsContext: () => ({ current: [] }) +})); + describe.concurrent("renderLore Tests", () => { beforeEach(() => { vi.useFakeTimers(); diff --git a/src/lib/shared/mc-text/mc-text.svelte.spec.ts b/src/lib/shared/mc-text/mc-text.svelte.spec.ts index 277dc866a..da91ad062 100644 --- a/src/lib/shared/mc-text/mc-text.svelte.spec.ts +++ b/src/lib/shared/mc-text/mc-text.svelte.spec.ts @@ -1,9 +1,10 @@ -import { setMaxEnchantments } from "$lib/shared/constants/enchantments"; import { mcTextToHTML } from "$lib/shared/mc-text"; import { colorCodes, extras } from "$lib/shared/mc-text/parser/styleLibrary"; -import { describe, it } from "vitest"; +import { describe, it, vi } from "vitest"; -setMaxEnchantments(["Aqua Affinity I"]); +vi.mock("$ctx", () => ({ + getEnchantmentsContext: () => ({ current: ["Aqua Affinity I"] }) +})); function sortedClasses(element: Element): string[] { return Array.from(element.classList).sort(); diff --git a/src/lib/shared/mc-text/parser/mcTextToHTML.ts b/src/lib/shared/mc-text/parser/mcTextToHTML.ts index 55c6f7ab6..f7996c5d2 100644 --- a/src/lib/shared/mc-text/parser/mcTextToHTML.ts +++ b/src/lib/shared/mc-text/parser/mcTextToHTML.ts @@ -1,4 +1,4 @@ -import { MAX_ENCHANTS } from "$lib/shared/constants/enchantments"; +import { isMaxEnchantment } from "$lib/shared/constants/enchantments"; import { colorCodes, extras, type ColorCodes, type FormattingCodes } from "$lib/shared/mc-text/parser/styleLibrary"; import { BASE_FORMATTING_CODE_REGEX, htmlStringFormatting } from "$lib/shared/mc-text/parser/utils"; @@ -101,7 +101,7 @@ export default function mcTextToHTML(...args: [{ mcString: string; breakLine?: b classList = classList.filter((className) => className !== "lore-enchantment"); // Check if the text contains max-level enchantments (for special rainbow effect) - shouldRainbowEnchantedCheck = MAX_ENCHANTS.has(normalizeEnchantText(item)); + shouldRainbowEnchantedCheck = isMaxEnchantment(normalizeEnchantText(item)); // Only create HTML elements for non-empty text content if (textContent !== "") { diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts deleted file mode 100644 index 201ffc171..000000000 --- a/src/routes/+layout.server.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { getEnchantments } from "$lib/shared/api/skycrypt-api.remote"; -import { setMaxEnchantments } from "$lib/shared/constants/enchantments"; -import type { LayoutServerLoad } from "./$types"; - -export const load = (async () => { - try { - const maxEnchantments = await getEnchantments(); - setMaxEnchantments(maxEnchantments); - return { maxEnchantments }; - } catch (error) { - console.error("Failed to load max-level enchantments", error); - setMaxEnchantments([]); - return { maxEnchantments: [] }; - } -}) satisfies LayoutServerLoad; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 382fa048c..746307a2d 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -11,8 +11,10 @@ initPreferences, initRecentSearches, initTheme, + EnchantmentsContext, PacksContext, setHoverContext, + setEnchantmentsContext, setMobileContext, setPacksContext } from "$ctx"; @@ -24,8 +26,7 @@ import { IsHover } from "$lib/hooks/is-hover.svelte"; import { IsMobile } from "$lib/hooks/is-mobile.svelte"; import { listLatestPostsForNotifications } from "$lib/shared/api/cms-api.remote"; - import { listResourcePacks } from "$lib/shared/api/skycrypt-api.remote"; - import { setMaxEnchantments } from "$lib/shared/constants/enchantments"; + import { listEnchantments, listResourcePacks } from "$lib/shared/api/skycrypt-api.remote"; import { parseThemeFromURL } from "$lib/shared/themes/sharing"; import * as Drawer from "$ui/drawer"; import { Separator } from "$ui/separator"; @@ -40,12 +41,8 @@ import "./layout.css"; import { SvelteURLSearchParams } from "svelte/reactivity"; import { writable } from "svelte/store"; - import type { LayoutData } from "./$types"; - let { children, data }: { children: Snippet; data: LayoutData } = $props(); - $effect.pre(() => { - setMaxEnchantments(data.maxEnchantments); - }); + let { children }: { children: Snippet } = $props(); let isMobile = $state(new IsMobile()); let isHover = $state(new IsHover()); let toastId: string | number = $state(0); @@ -68,6 +65,7 @@ const theme = writable("dark"); const noEmbedUrls = ["/stats/", "/newsroom"]; const packs = new PacksContext(); + const enchantments = new EnchantmentsContext(); const websiteJsonLd = { "@context": "https://schema.org", "@type": "WebSite", @@ -149,6 +147,7 @@ setMobileContext(isMobile); setHoverContext(isHover); setPacksContext(packs); + setEnchantmentsContext(enchantments); onMount(() => { if (window.innerWidth <= 600) { @@ -233,6 +232,13 @@ } }); + $effect(() => { + const query = listEnchantments(); + if (query.current) { + enchantments.current = query.current; + } + }); + let innerWidth = $state(0); From 655bccac7cf2ec3105697620f2de766c0126933c Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 14 Sep 2026 21:00:55 -0400 Subject: [PATCH 003/135] fix: deleting old file --- src/lib/shared/constants/enchantments.ts | 6 ------ src/lib/shared/mc-text/parser/mcTextToHTML.ts | 4 ++-- 2 files changed, 2 insertions(+), 8 deletions(-) delete mode 100644 src/lib/shared/constants/enchantments.ts diff --git a/src/lib/shared/constants/enchantments.ts b/src/lib/shared/constants/enchantments.ts deleted file mode 100644 index aa75f7cd8..000000000 --- a/src/lib/shared/constants/enchantments.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { getEnchantmentsContext } from "$ctx"; - -/** Check whether an enchantment is at its maximum level according to the backend constants. */ -export function isMaxEnchantment(enchantment: string): boolean { - return getEnchantmentsContext().current.includes(enchantment); -} diff --git a/src/lib/shared/mc-text/parser/mcTextToHTML.ts b/src/lib/shared/mc-text/parser/mcTextToHTML.ts index f7996c5d2..e41562e68 100644 --- a/src/lib/shared/mc-text/parser/mcTextToHTML.ts +++ b/src/lib/shared/mc-text/parser/mcTextToHTML.ts @@ -1,4 +1,4 @@ -import { isMaxEnchantment } from "$lib/shared/constants/enchantments"; +import { getEnchantmentsContext } from "$ctx"; import { colorCodes, extras, type ColorCodes, type FormattingCodes } from "$lib/shared/mc-text/parser/styleLibrary"; import { BASE_FORMATTING_CODE_REGEX, htmlStringFormatting } from "$lib/shared/mc-text/parser/utils"; @@ -101,7 +101,7 @@ export default function mcTextToHTML(...args: [{ mcString: string; breakLine?: b classList = classList.filter((className) => className !== "lore-enchantment"); // Check if the text contains max-level enchantments (for special rainbow effect) - shouldRainbowEnchantedCheck = isMaxEnchantment(normalizeEnchantText(item)); + shouldRainbowEnchantedCheck = getEnchantmentsContext().current.includes(normalizeEnchantText(item)); // Only create HTML elements for non-empty text content if (textContent !== "") { From 57e706c1ba2dd85d49b06cf2c1408c90331b2437 Mon Sep 17 00:00:00 2001 From: Carlos Date: Tue, 15 Sep 2026 17:56:44 -0700 Subject: [PATCH 004/135] fix: scope enchantment loading to stats --- src/lib/layouts/stats/Main.svelte | 12 +++++++++++- src/lib/shared/helper.ts | 6 ++++++ src/lib/shared/mc-text/parser/mcTextToHTML.ts | 4 ++-- src/routes/+layout.svelte | 13 +------------ .../stats/[ign]/[[profile]]/+page.svelte | 18 ++++++++++++++---- 5 files changed, 34 insertions(+), 19 deletions(-) diff --git a/src/lib/layouts/stats/Main.svelte b/src/lib/layouts/stats/Main.svelte index 709039466..ff35b7078 100644 --- a/src/lib/layouts/stats/Main.svelte +++ b/src/lib/layouts/stats/Main.svelte @@ -3,6 +3,7 @@ import { CombinedContext, AllStatsContext, + EnchantmentsContext, getHoverContext, getInternalState, getPreferences, @@ -11,6 +12,7 @@ ProfileContext, setAllStatsContext, setCombinedContext, + setEnchantmentsContext, setProfileContext } from "$ctx"; import { ContainedItemsGrid, ItemContent } from "$lib/components/item"; @@ -34,11 +36,13 @@ const { data: ctx, allStats, - combined + combined, + enchantments }: { data: ModelsStatsOutput; allStats: ModelsStatData[]; combined: ModelsCombinedOutput | null; + enchantments: string[]; } = $props(); const isHover = getHoverContext(); @@ -60,9 +64,11 @@ // Initialize the profile context const profileClass = new ProfileContext(); const allStatsClass = new AllStatsContext(); + const enchantmentsClass = new EnchantmentsContext(); const combinedClass = new CombinedContext(); setProfileContext(profileClass); setAllStatsContext(allStatsClass); + setEnchantmentsContext(enchantmentsClass); setCombinedContext(combinedClass); $effect.pre(() => { if (!ctx) return; @@ -99,6 +105,10 @@ $effect.pre(() => { allStatsClass.current = allStats ?? []; }); + + $effect.pre(() => { + enchantmentsClass.current = enchantments ?? []; + }); diff --git a/src/lib/shared/helper.ts b/src/lib/shared/helper.ts index ba3233ad4..22520e1e2 100644 --- a/src/lib/shared/helper.ts +++ b/src/lib/shared/helper.ts @@ -1,3 +1,4 @@ +import { getEnchantmentsContext } from "$ctx"; import { RARITY_COLORS } from "$lib/shared/constants/rarities"; import { mcTextToHTML } from "$lib/shared/mc-text"; import { tz } from "@date-fns/tz"; @@ -6,6 +7,11 @@ import prettyMilliseconds from "pretty-ms"; export { prettyMilliseconds as formatTime }; +/** Check whether an enchantment is at its maximum level according to the backend constants. */ +export function isMaxEnchantment(enchantment: string): boolean { + return getEnchantmentsContext().current.includes(enchantment); +} + /** * Formats a number using the compact notation (e.g., 1.2K, 3.4M). * diff --git a/src/lib/shared/mc-text/parser/mcTextToHTML.ts b/src/lib/shared/mc-text/parser/mcTextToHTML.ts index e41562e68..11c089a95 100644 --- a/src/lib/shared/mc-text/parser/mcTextToHTML.ts +++ b/src/lib/shared/mc-text/parser/mcTextToHTML.ts @@ -1,4 +1,4 @@ -import { getEnchantmentsContext } from "$ctx"; +import { isMaxEnchantment } from "$lib/shared/helper"; import { colorCodes, extras, type ColorCodes, type FormattingCodes } from "$lib/shared/mc-text/parser/styleLibrary"; import { BASE_FORMATTING_CODE_REGEX, htmlStringFormatting } from "$lib/shared/mc-text/parser/utils"; @@ -101,7 +101,7 @@ export default function mcTextToHTML(...args: [{ mcString: string; breakLine?: b classList = classList.filter((className) => className !== "lore-enchantment"); // Check if the text contains max-level enchantments (for special rainbow effect) - shouldRainbowEnchantedCheck = getEnchantmentsContext().current.includes(normalizeEnchantText(item)); + shouldRainbowEnchantedCheck = isMaxEnchantment(normalizeEnchantText(item)); // Only create HTML elements for non-empty text content if (textContent !== "") { diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 746307a2d..983bc6501 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -11,10 +11,8 @@ initPreferences, initRecentSearches, initTheme, - EnchantmentsContext, PacksContext, setHoverContext, - setEnchantmentsContext, setMobileContext, setPacksContext } from "$ctx"; @@ -26,7 +24,7 @@ import { IsHover } from "$lib/hooks/is-hover.svelte"; import { IsMobile } from "$lib/hooks/is-mobile.svelte"; import { listLatestPostsForNotifications } from "$lib/shared/api/cms-api.remote"; - import { listEnchantments, listResourcePacks } from "$lib/shared/api/skycrypt-api.remote"; + import { listResourcePacks } from "$lib/shared/api/skycrypt-api.remote"; import { parseThemeFromURL } from "$lib/shared/themes/sharing"; import * as Drawer from "$ui/drawer"; import { Separator } from "$ui/separator"; @@ -65,7 +63,6 @@ const theme = writable("dark"); const noEmbedUrls = ["/stats/", "/newsroom"]; const packs = new PacksContext(); - const enchantments = new EnchantmentsContext(); const websiteJsonLd = { "@context": "https://schema.org", "@type": "WebSite", @@ -147,7 +144,6 @@ setMobileContext(isMobile); setHoverContext(isHover); setPacksContext(packs); - setEnchantmentsContext(enchantments); onMount(() => { if (window.innerWidth <= 600) { @@ -232,13 +228,6 @@ } }); - $effect(() => { - const query = listEnchantments(); - if (query.current) { - enchantments.current = query.current; - } - }); - let innerWidth = $state(0); diff --git a/src/routes/stats/[ign]/[[profile]]/+page.svelte b/src/routes/stats/[ign]/[[profile]]/+page.svelte index 4c41bf3a4..8416523de 100644 --- a/src/routes/stats/[ign]/[[profile]]/+page.svelte +++ b/src/routes/stats/[ign]/[[profile]]/+page.svelte @@ -13,7 +13,8 @@ getAllStats, getCombinedProfileStats, getProfileStats, - getSelectedProfileStats + getSelectedProfileStats, + listEnchantments } from "$lib/shared/api/skycrypt-api.remote"; import LoaderCircle from "@lucide/svelte/icons/loader-circle"; import { type PageServerData } from "./$types"; @@ -29,6 +30,7 @@ async function loadProfileView(uuid: string, requestedProfileId: string | undefined) { const allStatsPromise = getAllStats(); + const enchantmentsPromise = listEnchantments(); const profile = requestedProfileId ? await getProfileStats({ uuid, profileId: requestedProfileId }) : await getSelectedProfileStats({ uuid }); @@ -36,9 +38,13 @@ profile.uuid && profile.profile_id ? getCombinedProfileStats({ uuid: profile.uuid, profileId: profile.profile_id }) : Promise.resolve(null); - const [allStats, combined] = await Promise.all([allStatsPromise, combinedPromise]); + const [allStats, combined, enchantments] = await Promise.all([ + allStatsPromise, + combinedPromise, + enchantmentsPromise + ]); - return { profile, allStats, combined }; + return { profile, allStats, combined, enchantments }; } function rewriteURL(profile: ModelsStatsOutput) { @@ -105,7 +111,11 @@ {#key routeKey} {const profileView = await profileViewPromise} -
+
{#snippet pending()} {@render loading()} From 1f9c4b48065604326effa286b307be870134cf28 Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 16 Sep 2026 18:52:19 -0700 Subject: [PATCH 005/135] fix: use generated enchantment response type --- src/context/createContext.svelte.ts | 7 ++++--- src/lib/layouts/stats/Main.svelte | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/context/createContext.svelte.ts b/src/context/createContext.svelte.ts index 09a349845..ae21e9162 100644 --- a/src/context/createContext.svelte.ts +++ b/src/context/createContext.svelte.ts @@ -6,7 +6,8 @@ import type { ModelsResourcePackConfig, ModelsStatData, ModelsSkillsOutput, - ModelsStatsOutput + ModelsStatsOutput, + listEnchantmentsResponse } from "$lib/shared/api/orval-generated"; import { createContext } from "svelte"; @@ -35,13 +36,13 @@ export class AllStatsContext { } export class EnchantmentsContext { - #current: string[] = $state([]); + #current: listEnchantmentsResponse["data"] = $state([]); get current() { return this.#current; } - set current(value: string[]) { + set current(value: listEnchantmentsResponse["data"]) { this.#current = value; } } diff --git a/src/lib/layouts/stats/Main.svelte b/src/lib/layouts/stats/Main.svelte index ff35b7078..d64ea2e08 100644 --- a/src/lib/layouts/stats/Main.svelte +++ b/src/lib/layouts/stats/Main.svelte @@ -23,7 +23,7 @@ import Skills from "$lib/layouts/stats/Skills.svelte"; import Stats from "$lib/layouts/stats/Stats.svelte"; import Sections from "$lib/sections/Sections.svelte"; - import type { ModelsCombinedOutput, ModelsStatData, ModelsStatsOutput } from "$lib/shared/api/orval-generated"; + import type { ModelsCombinedOutput, ModelsStatData, ModelsStatsOutput, listEnchantmentsResponse } from "$lib/shared/api/orval-generated"; import * as Dialog from "$ui/dialog"; import * as Drawer from "$ui/drawer"; import Image from "@lucide/svelte/icons/image"; @@ -42,7 +42,7 @@ data: ModelsStatsOutput; allStats: ModelsStatData[]; combined: ModelsCombinedOutput | null; - enchantments: string[]; + enchantments: listEnchantmentsResponse["data"]; } = $props(); const isHover = getHoverContext(); From 6ff47f5bf899bfacf9e4a384ac1038c83022fd0d Mon Sep 17 00:00:00 2001 From: DarthGigi <47110839+DarthGigi@users.noreply.github.com> Date: Sun, 20 Sep 2026 23:03:01 +0200 Subject: [PATCH 006/135] feat(stats): add Discord component embeds - Add secure component embed serialization with profile links and player data - Add embed generation tests and discord-api-types dependency --- .changeset/tangy-carpets-hear.md | 5 ++ package.json | 1 + pnpm-lock.yaml | 8 +++ src/lib/shared/discordEmbed.spec.ts | 38 ++++++++++++++ src/lib/shared/discordEmbed.ts | 49 +++++++++++++++++++ .../stats/[ign]/[[profile]]/+page.svelte | 9 ++++ 6 files changed, 110 insertions(+) create mode 100644 .changeset/tangy-carpets-hear.md create mode 100644 src/lib/shared/discordEmbed.spec.ts create mode 100644 src/lib/shared/discordEmbed.ts diff --git a/.changeset/tangy-carpets-hear.md b/.changeset/tangy-carpets-hear.md new file mode 100644 index 000000000..37eaca72a --- /dev/null +++ b/.changeset/tangy-carpets-hear.md @@ -0,0 +1,5 @@ +--- +"skycrypt-frontend": minor +--- + +Use the new component embeds for discord embeds diff --git a/package.json b/package.json index 9aa169d00..0dff9cce1 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "clsx": "^2.1.1", "date-fns": "^4.4.0", "devalue": "^5.9.1", + "discord-api-types": "^0.38.55", "dotenv-cli": "^11.0.0", "drizzle-kit": "^0.31.10", "drizzle-orm": "^0.45.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1212947da..a200fce1d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -147,6 +147,9 @@ importers: devalue: specifier: ^5.9.1 version: 5.9.1 + discord-api-types: + specifier: ^0.38.55 + version: 0.38.55 dotenv-cli: specifier: ^11.0.0 version: 11.0.0 @@ -3518,6 +3521,9 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + discord-api-types@0.38.55: + resolution: {integrity: sha512-ytuaRTzdnHUCXJ6KjL9MrItQX0xKncBKEeYI1Bst4+ud47eejH3cG6gaesYakjpPcUhh68XYS2YwGq3mmujFsA==} + dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} @@ -9171,6 +9177,8 @@ snapshots: dependencies: dequal: 2.0.3 + discord-api-types@0.38.55: {} + dlv@1.1.3: optional: true diff --git a/src/lib/shared/discordEmbed.spec.ts b/src/lib/shared/discordEmbed.spec.ts new file mode 100644 index 000000000..4e39bb636 --- /dev/null +++ b/src/lib/shared/discordEmbed.spec.ts @@ -0,0 +1,38 @@ +import { expect, it } from "vitest"; +import { createDiscordEmbed, serializeDiscordEmbed } from "./discordEmbed"; + +it("keeps embedded JSON inside its script element", () => { + const embed = createDiscordEmbed({ username: "&" }); + const json = serializeDiscordEmbed(embed); + expect(json).not.toContain("<"); + expect(JSON.parse(json)).toEqual(embed); +}); + +it("uses the profile data and puts a separator directly above valid link buttons", () => { + const embed = createDiscordEmbed({ + username: "tiltedhoney", + profile_cute_name: "Kiwi", + uuid: "debb9e1f6dcb4f5b83c291fd5388bb56", + rank: { rankText: "MVP", plusText: "+" }, + skyblock_level: 620.16 + }); + const [section, separator, row, footer] = embed.component.components; + expect(section).toMatchObject({ + type: 9, + components: [ + { content: expect.stringContaining("### [tiltedhoney (Kiwi)](https://sky.shiiyu.moe/stats/tiltedhoney/Kiwi)") } + ], + accessory: { type: 11, media: { url: "https://nmsr.nickac.dev/bust/debb9e1f6dcb4f5b83c291fd5388bb56?y=-20" } } + }); + expect(JSON.stringify(section)).toContain("🌟 Level: 620.16"); + expect(separator).toEqual({ type: 14, divider: true, spacing: 1 }); + expect(row).toEqual({ + type: 1, + components: [ + { type: 2, style: 5, label: "SkyCrypt", url: "https://sky.shiiyu.moe/stats/tiltedhoney/Kiwi" }, + { type: 2, style: 5, label: "Plancke", url: "https://plancke.io/hypixel/player/stats/tiltedhoney" }, + { type: 2, style: 5, label: "Elite", url: "https://eliteskyblock.com/@tiltedhoney/Kiwi" } + ] + }); + expect(footer).toEqual({ type: 10, content: `-# SkyCrypt • v${__NPM_PACKAGE_VERSION__}` }); +}); diff --git a/src/lib/shared/discordEmbed.ts b/src/lib/shared/discordEmbed.ts new file mode 100644 index 000000000..5be3b0223 --- /dev/null +++ b/src/lib/shared/discordEmbed.ts @@ -0,0 +1,49 @@ +import type { ModelsEmbedData } from "$lib/shared/api/orval-generated"; +import { getLongDescription } from "$lib/shared/embedGenerator"; +import type { APIContainerComponent } from "discord-api-types/v10"; + +export function createDiscordEmbed(data: ModelsEmbedData) { + const username = encodeURIComponent(data.username || ""); + const profile = encodeURIComponent(data.profile_cute_name || ""); + const profileUrl = `https://sky.shiiyu.moe/stats/${username}/${profile}`; + const title = `${data.displayName || data.username || "SkyCrypt"} (${data.profile_cute_name || "Selected profile"})` + .replace(/[\\[\]*_`<>]/g, "\\$&") + .replace(/[\r\n]/g, " "); + const summary = { + type: 10 as const, + content: `### [${title}](${profileUrl})\n${getLongDescription(data).trim()}` + }; + + return { + component: { + type: 17, + accent_color: 1957930, + components: [ + data.uuid + ? { + type: 9, + components: [summary], + accessory: { + type: 11, + media: { url: `https://nmsr.nickac.dev/bust/${encodeURIComponent(data.uuid)}?y=-20` } + } + } + : summary, + { type: 14, divider: true, spacing: 1 }, + { + type: 1, + components: [ + { type: 2, style: 5, label: "SkyCrypt", url: profileUrl }, + { type: 2, style: 5, label: "Plancke", url: `https://plancke.io/hypixel/player/stats/${username}` }, + { type: 2, style: 5, label: "Elite", url: `https://eliteskyblock.com/@${username}/${profile}` } + ] + }, + { type: 10, content: `-# SkyCrypt • v${__NPM_PACKAGE_VERSION__}` } + ] + } + } satisfies { component: APIContainerComponent }; +} + +export function serializeDiscordEmbed(embed: { component: APIContainerComponent }): string { + return JSON.stringify(embed).replace(//g, "\\u003e").replace(/&/g, "\\u0026"); +} diff --git a/src/routes/stats/[ign]/[[profile]]/+page.svelte b/src/routes/stats/[ign]/[[profile]]/+page.svelte index 4c41bf3a4..e0a52cc24 100644 --- a/src/routes/stats/[ign]/[[profile]]/+page.svelte +++ b/src/routes/stats/[ign]/[[profile]]/+page.svelte @@ -15,10 +15,12 @@ getProfileStats, getSelectedProfileStats } from "$lib/shared/api/skycrypt-api.remote"; + import { createDiscordEmbed, serializeDiscordEmbed } from "$lib/shared/discordEmbed"; import LoaderCircle from "@lucide/svelte/icons/loader-circle"; import { type PageServerData } from "./$types"; const { data }: { data: PageServerData } = $props(); + const discordEmbed = $derived(createDiscordEmbed(data.embed)); const preferences = getPreferences(); const internalState = getInternalState(); @@ -87,6 +89,13 @@ }); + + {#if data.embed.username} + + {serializeDiscordEmbed(discordEmbed)} + {/if} + + {#if data.embed} {/if} From 17a2652478b09ca460c6771489a4867cfde1b19b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 20 Sep 2026 21:03:44 +0000 Subject: [PATCH 007/135] chore: version packages (beta) [skip ci] --- .changeset/pre.json | 4 ++++ .changeset/{ => pre}/tangy-carpets-hear.md | 0 CHANGELOG.md | 6 ++++++ package.json | 2 +- 4 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 .changeset/pre.json rename .changeset/{ => pre}/tangy-carpets-hear.md (100%) diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 000000000..f6799931c --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,4 @@ +{ + "mode": "pre", + "tag": "beta" +} diff --git a/.changeset/tangy-carpets-hear.md b/.changeset/pre/tangy-carpets-hear.md similarity index 100% rename from .changeset/tangy-carpets-hear.md rename to .changeset/pre/tangy-carpets-hear.md diff --git a/CHANGELOG.md b/CHANGELOG.md index fb9706398..6fb4d169f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 4.1.0-beta.0 + +### Minor Changes + +- Use the new component embeds for discord embeds _[`6ff47f5`](https://github.com/SkyCryptWebsite/SkyCrypt-Frontend/commit/6ff47f5bf899bfacf9e4a384ac1038c83022fd0d) [@DarthGigi](https://github.com/DarthGigi)_ + ## 4.0.0 ### Major Changes diff --git a/package.json b/package.json index 0dff9cce1..eceb7c08f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skycrypt-frontend", - "version": "4.0.0", + "version": "4.1.0-beta.0", "private": true, "license": "AGPL-3.0-only", "repository": { From 93f541482372b322ceb5246b29842068fc14ec3c Mon Sep 17 00:00:00 2001 From: DarthGigi <47110839+DarthGigi@users.noreply.github.com> Date: Sun, 20 Sep 2026 23:32:53 +0200 Subject: [PATCH 008/135] fix(cache): version prerendered endpoint URLs - Preserve saved resource-pack preferences across builds - Display the prerender version and add cache regression coverage --- .changeset/fresh-prerender-cache.md | 5 ++ .github/workflows/website.yml | 2 +- e2e/packs-cache.spec.ts | 44 ++++++++++ src/app.d.ts | 1 + src/lib/components/header/Info.svelte | 4 + src/lib/shared/api/skycrypt-api.remote.ts | 28 +++--- src/routes/+layout.svelte | 2 +- src/routes/+page.svelte | 2 +- src/routes/contributors.remote.ts | 87 ++++++++++--------- src/routes/sitemap.xml/+server.ts | 2 +- .../stats/[ign]/[[profile]]/+page.svelte | 2 +- vite.config.ts | 5 ++ 12 files changed, 127 insertions(+), 57 deletions(-) create mode 100644 .changeset/fresh-prerender-cache.md create mode 100644 e2e/packs-cache.spec.ts diff --git a/.changeset/fresh-prerender-cache.md b/.changeset/fresh-prerender-cache.md new file mode 100644 index 000000000..950786f26 --- /dev/null +++ b/.changeset/fresh-prerender-cache.md @@ -0,0 +1,5 @@ +--- +"skycrypt-frontend": patch +--- + +Version prerendered endpoint URLs per build so updated resource packs and other static data appear without clearing browser caches. Preserve saved pack preferences and the commit-based app version, and show the separate prerender version in About. diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index 49739894b..fddc7abd5 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -2,7 +2,7 @@ name: Website on: push: - paths: ["src/**", "Dockerfile", "package.json", ".changeset/**", ".github/workflows/website.yml"] + paths: ["src/**", "Dockerfile", "package.json", "vite.config.ts", ".changeset/**", ".github/workflows/website.yml"] workflow_dispatch: env: diff --git a/e2e/packs-cache.spec.ts b/e2e/packs-cache.spec.ts new file mode 100644 index 000000000..a7646fe33 --- /dev/null +++ b/e2e/packs-cache.spec.ts @@ -0,0 +1,44 @@ +import { expect, test } from "@playwright/test"; + +test("uses a versioned prerendered catalog without resetting saved pack choices", async ({ page }) => { + const responsePromise = page.waitForResponse((response) => response.url().includes("/listResourcePacks")); + await page.goto("/"); + const response = await responsePromise; + const catalogUrl = response.url(); + expect(response.status()).toBe(200); + expect(new URL(catalogUrl).pathname).toMatch(/\/listResourcePacks\/[^/]+$/); + + await page.getByRole("button", { name: "Settings", exact: true }).click(); + await page.getByRole("tab", { name: /^packs$/i }).click(); + const rows = page.locator('[data-slot="resource-pack-row"]'); + await expect(rows.first()).toBeVisible(); + const ids = await rows.evaluateAll((elements) => elements.map((element) => element.getAttribute("data-pack-id"))); + + await page.evaluate(async (url) => { + localStorage.setItem("skycryptEnabledPacks", JSON.stringify(["FSR"])); + document.cookie = `enabledPacks=${encodeURIComponent(JSON.stringify(["FSR"]))}; path=/`; + const unversionedUrl = url.slice(0, url.indexOf("/listResourcePacks") + "/listResourcePacks".length); + for (const name of await caches.keys()) { + if (!name.startsWith("sveltekit:")) continue; + const cache = await caches.open(name); + await cache.delete(url); + // Old releases cached this URL without any build identifier. + await cache.put(unversionedUrl, new Response("[[]]")); + } + await (await caches.open("sveltekit:previous-build")).put(unversionedUrl, new Response("[[]]")); + }, catalogUrl); + + const refreshed = page.waitForResponse((response) => response.url() === catalogUrl); + await page.reload(); + expect((await refreshed).status()).toBe(200); + await page.getByRole("button", { name: "Settings", exact: true }).click(); + await page.getByRole("tab", { name: /^packs$/i }).click(); + await expect(rows).toHaveCount(ids.length); + for (const id of ids) { + await expect(page.locator(`[data-slot="resource-pack-row"][data-pack-id="${id}"]`)).toHaveAttribute( + "data-enabled", + id === "FSR" ? "true" : "false" + ); + } + expect(await page.evaluate(() => caches.keys())).not.toContain("sveltekit:previous-build"); +}); diff --git a/src/app.d.ts b/src/app.d.ts index 031435aaa..f9b2ae91b 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -4,6 +4,7 @@ import type { Session, User } from "better-auth/minimal"; // for information about these interfaces declare global { const __NPM_PACKAGE_VERSION__: string; + const __PRERENDER_VERSION__: string; namespace App { // interface Error {} diff --git a/src/lib/components/header/Info.svelte b/src/lib/components/header/Info.svelte index 804ed4d7f..0f0b1293a 100644 --- a/src/lib/components/header/Info.svelte +++ b/src/lib/components/header/Info.svelte @@ -9,6 +9,7 @@ const { PUBLIC_DISCORD_INVITE, PUBLIC_PATREON } = env; const packageVersion = __NPM_PACKAGE_VERSION__; const commitHash = PUBLIC_COMMIT_HASH; + const prerenderVersion = __PRERENDER_VERSION__;

SkyCrypt is a free, open-source stats viewer for Hypixel SkyBlock.

@@ -64,6 +65,9 @@

{/if} +

+ Prerender version {prerenderVersion} +

You can report bugs, suggest features on = TResponse extends { data: infer TData } @@ -127,19 +128,24 @@ export const resolveUuidByUsername = query(ResolveUuidByUsernameParams, async ({ }); /** Resolve a UUID to a username */ -export const resolveUsernameByUuid = prerender(ResolveUsernameByUuidParams, async ({ uuid }) => { - return fetchSection(APIEndpointName.USERNAME, () => resolveUsernameByUuidRequest(uuid)); -}); - -/** Fetch resource packs */ -export const listResourcePacks = prerender(async () => { - return fetchSection(APIEndpointName.RESOURCEPACK, () => listResourcePacksRequest()); -}); +export const resolveUsernameByUuid = prerender( + ResolveUsernameByUuidParams.extend({ version: z.string() }), + async ({ uuid }) => fetchSection(APIEndpointName.USERNAME, () => resolveUsernameByUuidRequest(uuid)) +); + +/** Give each build its own static catalog URL without changing the app's commit version. */ +export const listResourcePacks = prerender( + z.string(), + async (_version) => fetchSection(APIEndpointName.RESOURCEPACK, () => listResourcePacksRequest()), + { inputs: () => [__PRERENDER_VERSION__] } +); /** Fetch stats list */ -export const getAllStats = prerender(async () => { - return fetchSection(APIEndpointName.STATS, () => getStatsConstantsRequest()); -}); +export const getAllStats = prerender( + z.string(), + async (_version) => fetchSection(APIEndpointName.STATS, () => getStatsConstantsRequest()), + { inputs: () => [__PRERENDER_VERSION__] } +); /** Fetch the source information for the currently running backend service. */ export const getSourceInfo = query(async (): Promise => { diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 983bc6501..a5c373ff9 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -221,7 +221,7 @@ }); $effect(() => { - const query = listResourcePacks(); + const query = listResourcePacks(__PRERENDER_VERSION__); if (query.current) { packs.packs = query.current; enabledPacks.configure(query.current); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index cc49a7512..79fc1499e 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -273,7 +273,7 @@ {/snippet} - {#each await getContributors() as contributor (contributor.id)} + {#each await getContributors(__PRERENDER_VERSION__) as contributor (contributor.id)} {/each} diff --git a/src/routes/contributors.remote.ts b/src/routes/contributors.remote.ts index f09069055..03eac38bd 100644 --- a/src/routes/contributors.remote.ts +++ b/src/routes/contributors.remote.ts @@ -1,6 +1,7 @@ import { prerender } from "$app/server"; import type { ModelsPlayerResolve } from "$lib/shared/api/orval-generated"; import { resolveUsernameByUuid } from "$lib/shared/api/skycrypt-api.remote"; +import { z } from "zod"; import { Role } from "./enums"; export interface Contributor extends ModelsPlayerResolve { @@ -9,48 +10,52 @@ export interface Contributor extends ModelsPlayerResolve { role?: Role; } -export const getContributors = prerender(async () => { - const getUsernames = async (uuid: string): Promise => { - try { - return await resolveUsernameByUuid({ uuid }); - } catch (error) { - console.error("Something went wrong fetching contributor username for UUID:", uuid, error); - return { - displayName: "???", - uuid: uuid, - username: "???" - }; - } - }; +export const getContributors = prerender( + z.string(), + async (version) => { + const getUsernames = async (uuid: string): Promise => { + try { + return await resolveUsernameByUuid({ uuid, version }); + } catch (error) { + console.error("Something went wrong fetching contributor username for UUID:", uuid, error); + return { + displayName: "???", + uuid: uuid, + username: "???" + }; + } + }; - const contributors = async () => { - const ids = [ - { id: "dec987bbfa734d63bb9d566139232f81", quote: "btw i use arch", role: Role.MAINTAINER }, - { - id: "91f57dfc7f5845c9a4920f6c16640038", - quote: "A useless human being that delivers nothing to society", - role: Role.MAINTAINER - }, - { id: "b44d2d5272dc49c28185b2d6a158d80a", quote: "nyaaaa~ :3", role: Role.MAINTAINER }, - { id: "f5667ad6b4b3434ba58f2ed2396f62f2", quote: '"lazy dev"   (ノ´・ω・)ノ ミ ┸━┸', role: Role.MAINTAINER }, - { id: "20d6334b7f9541ebbf7f860205ebf846", quote: "bob", role: Role.MAINTAINER }, - { id: "aad581b2f90048a785a7573d31d7b862", quote: "ember armor no longer on top :((", role: Role.MAINTAINER }, - { id: "d705483c5157460dad39712e4d74dfe1", quote: "Last online: January 1st, 1970", role: Role.FACILITATOR }, - { id: "7d7b04b49c24433cacc60a3da66eba01", quote: "🍞Grandmaster of Unit Testing", role: Role.FACILITATOR }, - { id: "1915444928b64d8b8973df8044f8cdb7", quote: "lea plant", role: Role.CONTRIBUTOR }, - { id: "8a3fa60d87aa4240bcdc624b90632529", quote: "Nate: CSS Wizard", role: Role.CONTRIBUTOR }, - { id: "5435b597612f4554a3c651fd1c3ee96a", quote: "/visit dukioooo", role: Role.CONTRIBUTOR }, - { id: "b876ec32e396476ba1158438d83c67d4", quote: "Long live the Potato king!", role: Role.TECHNOBLADE } - ]; + const contributors = async () => { + const ids = [ + { id: "dec987bbfa734d63bb9d566139232f81", quote: "btw i use arch", role: Role.MAINTAINER }, + { + id: "91f57dfc7f5845c9a4920f6c16640038", + quote: "A useless human being that delivers nothing to society", + role: Role.MAINTAINER + }, + { id: "b44d2d5272dc49c28185b2d6a158d80a", quote: "nyaaaa~ :3", role: Role.MAINTAINER }, + { id: "f5667ad6b4b3434ba58f2ed2396f62f2", quote: '"lazy dev"   (ノ´・ω・)ノ ミ ┸━┸', role: Role.MAINTAINER }, + { id: "20d6334b7f9541ebbf7f860205ebf846", quote: "bob", role: Role.MAINTAINER }, + { id: "aad581b2f90048a785a7573d31d7b862", quote: "ember armor no longer on top :((", role: Role.MAINTAINER }, + { id: "d705483c5157460dad39712e4d74dfe1", quote: "Last online: January 1st, 1970", role: Role.FACILITATOR }, + { id: "7d7b04b49c24433cacc60a3da66eba01", quote: "🍞Grandmaster of Unit Testing", role: Role.FACILITATOR }, + { id: "1915444928b64d8b8973df8044f8cdb7", quote: "lea plant", role: Role.CONTRIBUTOR }, + { id: "8a3fa60d87aa4240bcdc624b90632529", quote: "Nate: CSS Wizard", role: Role.CONTRIBUTOR }, + { id: "5435b597612f4554a3c651fd1c3ee96a", quote: "/visit dukioooo", role: Role.CONTRIBUTOR }, + { id: "b876ec32e396476ba1158438d83c67d4", quote: "Long live the Potato king!", role: Role.TECHNOBLADE } + ]; - const usernames = await Promise.all(ids.map(({ id }) => getUsernames(id))); + const usernames = await Promise.all(ids.map(({ id }) => getUsernames(id))); - return ids.map((contributor, index) => ({ - ...contributor, - name: usernames[index].username, - displayName: usernames[index].displayName - })) satisfies Contributor[]; - }; + return ids.map((contributor, index) => ({ + ...contributor, + name: usernames[index].username, + displayName: usernames[index].displayName + })) satisfies Contributor[]; + }; - return contributors(); -}); + return contributors(); + }, + { inputs: () => [__PRERENDER_VERSION__] } +); diff --git a/src/routes/sitemap.xml/+server.ts b/src/routes/sitemap.xml/+server.ts index 44ccc0104..c4469cc38 100644 --- a/src/routes/sitemap.xml/+server.ts +++ b/src/routes/sitemap.xml/+server.ts @@ -9,7 +9,7 @@ const statsPaths = new Set(); export const GET: RequestHandler = async () => { try { - const contributors = await getContributors(); + const contributors = await getContributors(__PRERENDER_VERSION__); for (const contributor of contributors) { const ign = contributor.name ?? contributor.id; diff --git a/src/routes/stats/[ign]/[[profile]]/+page.svelte b/src/routes/stats/[ign]/[[profile]]/+page.svelte index e0a52cc24..f9131c141 100644 --- a/src/routes/stats/[ign]/[[profile]]/+page.svelte +++ b/src/routes/stats/[ign]/[[profile]]/+page.svelte @@ -30,7 +30,7 @@ const profileViewPromise = $derived(loadProfileView(ign, profileId)); async function loadProfileView(uuid: string, requestedProfileId: string | undefined) { - const allStatsPromise = getAllStats(); + const allStatsPromise = getAllStats(__PRERENDER_VERSION__); const profile = requestedProfileId ? await getProfileStats({ uuid, profileId: requestedProfileId }) : await getSelectedProfileStats({ uuid }); diff --git a/vite.config.ts b/vite.config.ts index 8b01686a1..70dbdd91c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,3 +1,4 @@ +import { randomUUID } from "node:crypto"; import { sentrySvelteKit } from "@sentry/sveltekit"; import adapter from "@sveltejs/adapter-node"; import { sveltekit } from "@sveltejs/kit/vite"; @@ -6,6 +7,9 @@ import tailwindcss from "@tailwindcss/vite"; import { playwright } from "@vitest/browser-playwright"; import { defineConfig } from "vitest/config"; +// SvelteKit reloads this config for the client build; both bundles need the same prerender ID. +const prerenderVersion = (process.env.SKYCRYPT_PRERENDER_VERSION ??= randomUUID()); + export default defineConfig({ plugins: [ sentrySvelteKit({ @@ -137,6 +141,7 @@ export default defineConfig({ ] }, define: { + __PRERENDER_VERSION__: JSON.stringify(prerenderVersion), __NPM_PACKAGE_VERSION__: JSON.stringify(process.env.npm_package_version || "") } }); From a00b55de339fb608a356ffbbe0984f85b03239c5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 20 Sep 2026 21:33:46 +0000 Subject: [PATCH 009/135] chore: version packages (beta) [skip ci] --- .changeset/{ => pre}/fresh-prerender-cache.md | 0 CHANGELOG.md | 6 ++++++ package.json | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) rename .changeset/{ => pre}/fresh-prerender-cache.md (100%) diff --git a/.changeset/fresh-prerender-cache.md b/.changeset/pre/fresh-prerender-cache.md similarity index 100% rename from .changeset/fresh-prerender-cache.md rename to .changeset/pre/fresh-prerender-cache.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fb4d169f..507c7fb54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 4.1.0-beta.1 + +### Patch Changes + +- Version prerendered endpoint URLs per build so updated resource packs and other static data appear without clearing browser caches. Preserve saved pack preferences and the commit-based app version, and show the separate prerender version in About. _[`93f5414`](https://github.com/SkyCryptWebsite/SkyCrypt-Frontend/commit/93f541482372b322ceb5246b29842068fc14ec3c) [@DarthGigi](https://github.com/DarthGigi)_ + ## 4.1.0-beta.0 ### Minor Changes diff --git a/package.json b/package.json index eceb7c08f..20f76ea0e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skycrypt-frontend", - "version": "4.1.0-beta.0", + "version": "4.1.0-beta.1", "private": true, "license": "AGPL-3.0-only", "repository": { From 0e7bc8ec985c36b6369a746fd56c83bec1272178 Mon Sep 17 00:00:00 2001 From: DarthGigi <47110839+DarthGigi@users.noreply.github.com> Date: Sun, 20 Sep 2026 23:37:05 +0200 Subject: [PATCH 010/135] feat(discord-embed): add UTM tracking to profile links - Append SkyCrypt campaign parameters to all profile buttons - Update embed link expectations --- src/lib/shared/discordEmbed.spec.ts | 21 ++++++++++++++++++--- src/lib/shared/discordEmbed.ts | 17 ++++++++++++++--- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/lib/shared/discordEmbed.spec.ts b/src/lib/shared/discordEmbed.spec.ts index 4e39bb636..1f57de1ad 100644 --- a/src/lib/shared/discordEmbed.spec.ts +++ b/src/lib/shared/discordEmbed.spec.ts @@ -29,9 +29,24 @@ it("uses the profile data and puts a separator directly above valid link buttons expect(row).toEqual({ type: 1, components: [ - { type: 2, style: 5, label: "SkyCrypt", url: "https://sky.shiiyu.moe/stats/tiltedhoney/Kiwi" }, - { type: 2, style: 5, label: "Plancke", url: "https://plancke.io/hypixel/player/stats/tiltedhoney" }, - { type: 2, style: 5, label: "Elite", url: "https://eliteskyblock.com/@tiltedhoney/Kiwi" } + { + type: 2, + style: 5, + label: "SkyCrypt", + url: "https://sky.shiiyu.moe/stats/tiltedhoney/Kiwi?utm_source=SkyCrypt&utm_campaign=DiscordEmbed" + }, + { + type: 2, + style: 5, + label: "Plancke", + url: "https://plancke.io/hypixel/player/stats/tiltedhoney?utm_source=SkyCrypt&utm_campaign=DiscordEmbed" + }, + { + type: 2, + style: 5, + label: "Elite", + url: "https://eliteskyblock.com/@tiltedhoney/Kiwi?utm_source=SkyCrypt&utm_campaign=DiscordEmbed" + } ] }); expect(footer).toEqual({ type: 10, content: `-# SkyCrypt • v${__NPM_PACKAGE_VERSION__}` }); diff --git a/src/lib/shared/discordEmbed.ts b/src/lib/shared/discordEmbed.ts index 5be3b0223..3247bf10a 100644 --- a/src/lib/shared/discordEmbed.ts +++ b/src/lib/shared/discordEmbed.ts @@ -6,6 +6,7 @@ export function createDiscordEmbed(data: ModelsEmbedData) { const username = encodeURIComponent(data.username || ""); const profile = encodeURIComponent(data.profile_cute_name || ""); const profileUrl = `https://sky.shiiyu.moe/stats/${username}/${profile}`; + const trackingParams = "?utm_source=SkyCrypt&utm_campaign=DiscordEmbed"; const title = `${data.displayName || data.username || "SkyCrypt"} (${data.profile_cute_name || "Selected profile"})` .replace(/[\\[\]*_`<>]/g, "\\$&") .replace(/[\r\n]/g, " "); @@ -33,9 +34,19 @@ export function createDiscordEmbed(data: ModelsEmbedData) { { type: 1, components: [ - { type: 2, style: 5, label: "SkyCrypt", url: profileUrl }, - { type: 2, style: 5, label: "Plancke", url: `https://plancke.io/hypixel/player/stats/${username}` }, - { type: 2, style: 5, label: "Elite", url: `https://eliteskyblock.com/@${username}/${profile}` } + { type: 2, style: 5, label: "SkyCrypt", url: `${profileUrl}${trackingParams}` }, + { + type: 2, + style: 5, + label: "Plancke", + url: `https://plancke.io/hypixel/player/stats/${username}${trackingParams}` + }, + { + type: 2, + style: 5, + label: "Elite", + url: `https://eliteskyblock.com/@${username}/${profile}${trackingParams}` + } ] }, { type: 10, content: `-# SkyCrypt • v${__NPM_PACKAGE_VERSION__}` } From 6d56391ec2cde7cf39b2bb8f8aa49e09f51a8ded Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 20 Sep 2026 21:42:48 +0000 Subject: [PATCH 011/135] chore: version packages (stable) [skip ci] --- .changeset/pre.json | 4 ---- .changeset/pre/fresh-prerender-cache.md | 5 ----- .changeset/pre/tangy-carpets-hear.md | 5 ----- CHANGELOG.md | 10 ++++++++++ package.json | 2 +- 5 files changed, 11 insertions(+), 15 deletions(-) delete mode 100644 .changeset/pre.json delete mode 100644 .changeset/pre/fresh-prerender-cache.md delete mode 100644 .changeset/pre/tangy-carpets-hear.md diff --git a/.changeset/pre.json b/.changeset/pre.json deleted file mode 100644 index f6799931c..000000000 --- a/.changeset/pre.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mode": "pre", - "tag": "beta" -} diff --git a/.changeset/pre/fresh-prerender-cache.md b/.changeset/pre/fresh-prerender-cache.md deleted file mode 100644 index 950786f26..000000000 --- a/.changeset/pre/fresh-prerender-cache.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"skycrypt-frontend": patch ---- - -Version prerendered endpoint URLs per build so updated resource packs and other static data appear without clearing browser caches. Preserve saved pack preferences and the commit-based app version, and show the separate prerender version in About. diff --git a/.changeset/pre/tangy-carpets-hear.md b/.changeset/pre/tangy-carpets-hear.md deleted file mode 100644 index 37eaca72a..000000000 --- a/.changeset/pre/tangy-carpets-hear.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"skycrypt-frontend": minor ---- - -Use the new component embeds for discord embeds diff --git a/CHANGELOG.md b/CHANGELOG.md index 507c7fb54..158536cb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 4.1.0 + +### Minor Changes + +- Use the new component embeds for discord embeds _[`#347`](https://github.com/SkyCryptWebsite/SkyCrypt-Frontend/pull/347) [`6ff47f5`](https://github.com/SkyCryptWebsite/SkyCrypt-Frontend/commit/6ff47f5bf899bfacf9e4a384ac1038c83022fd0d) [@DarthGigi](https://github.com/DarthGigi)_ + +### Patch Changes + +- Version prerendered endpoint URLs per build so updated resource packs and other static data appear without clearing browser caches. Preserve saved pack preferences and the commit-based app version, and show the separate prerender version in About. _[`#347`](https://github.com/SkyCryptWebsite/SkyCrypt-Frontend/pull/347) [`93f5414`](https://github.com/SkyCryptWebsite/SkyCrypt-Frontend/commit/93f541482372b322ceb5246b29842068fc14ec3c) [@DarthGigi](https://github.com/DarthGigi)_ + ## 4.1.0-beta.1 ### Patch Changes diff --git a/package.json b/package.json index 20f76ea0e..6a273c7f3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skycrypt-frontend", - "version": "4.1.0-beta.1", + "version": "4.1.0", "private": true, "license": "AGPL-3.0-only", "repository": { From c027c31cc48c2f260004d4d0f7dc032063c3af25 Mon Sep 17 00:00:00 2001 From: DarthGigi <47110839+DarthGigi@users.noreply.github.com> Date: Sun, 20 Sep 2026 23:57:35 +0200 Subject: [PATCH 012/135] fix(discord): use valid rank colors in embeds - Prefer the player's plus color, then rank color - Fall back to the default preview color when invalid or unavailable --- .changeset/rank-colored-discord-embeds.md | 5 +++++ src/lib/shared/discordEmbed.spec.ts | 11 +++++++++++ src/lib/shared/discordEmbed.ts | 4 +++- 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 .changeset/rank-colored-discord-embeds.md diff --git a/.changeset/rank-colored-discord-embeds.md b/.changeset/rank-colored-discord-embeds.md new file mode 100644 index 000000000..e6df4b0e5 --- /dev/null +++ b/.changeset/rank-colored-discord-embeds.md @@ -0,0 +1,5 @@ +--- +"skycrypt-frontend": patch +--- + +Use the player's rank color for Discord component embeds, preferring the plus color and falling back to the default preview color when no valid rank color is available. diff --git a/src/lib/shared/discordEmbed.spec.ts b/src/lib/shared/discordEmbed.spec.ts index 1f57de1ad..f8342f5b3 100644 --- a/src/lib/shared/discordEmbed.spec.ts +++ b/src/lib/shared/discordEmbed.spec.ts @@ -1,6 +1,17 @@ import { expect, it } from "vitest"; import { createDiscordEmbed, serializeDiscordEmbed } from "./discordEmbed"; +it.each([ + { rank: { plusColor: "#FF5555", rankColor: "#55FFFF" }, expected: 0xff5555 }, + { rank: { rankColor: "#55ffff" }, expected: 0x55ffff }, + { rank: { plusColor: "#000000", rankColor: "#55FFFF" }, expected: 0 }, + { rank: { plusColor: "invalid", rankColor: "#55FFFF" }, expected: 0x55ffff }, + { rank: { rankColor: "#12345678" }, expected: 0x282828 }, + { rank: undefined, expected: 0x282828 } +])("uses the preferred valid rank color: $rank", ({ rank, expected }) => { + expect(createDiscordEmbed({ username: "tiltedhoney", rank }).component.accent_color).toBe(expected); +}); + it("keeps embedded JSON inside its script element", () => { const embed = createDiscordEmbed({ username: "&" }); const json = serializeDiscordEmbed(embed); diff --git a/src/lib/shared/discordEmbed.ts b/src/lib/shared/discordEmbed.ts index 3247bf10a..fc1fa0ae7 100644 --- a/src/lib/shared/discordEmbed.ts +++ b/src/lib/shared/discordEmbed.ts @@ -3,6 +3,8 @@ import { getLongDescription } from "$lib/shared/embedGenerator"; import type { APIContainerComponent } from "discord-api-types/v10"; export function createDiscordEmbed(data: ModelsEmbedData) { + const color = + [data.rank?.plusColor, data.rank?.rankColor].find((value) => /^#[\da-f]{6}$/i.test(value ?? "")) || "#282828"; const username = encodeURIComponent(data.username || ""); const profile = encodeURIComponent(data.profile_cute_name || ""); const profileUrl = `https://sky.shiiyu.moe/stats/${username}/${profile}`; @@ -18,7 +20,7 @@ export function createDiscordEmbed(data: ModelsEmbedData) { return { component: { type: 17, - accent_color: 1957930, + accent_color: Number.parseInt(color.slice(1), 16), components: [ data.uuid ? { From 1081ea3bae54aaf57b46662261b42d79b9b38e72 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 20 Sep 2026 21:58:15 +0000 Subject: [PATCH 013/135] chore: version packages (beta) [skip ci] --- .changeset/pre.json | 4 ++++ .changeset/{ => pre}/rank-colored-discord-embeds.md | 0 CHANGELOG.md | 6 ++++++ package.json | 2 +- 4 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 .changeset/pre.json rename .changeset/{ => pre}/rank-colored-discord-embeds.md (100%) diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 000000000..f6799931c --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,4 @@ +{ + "mode": "pre", + "tag": "beta" +} diff --git a/.changeset/rank-colored-discord-embeds.md b/.changeset/pre/rank-colored-discord-embeds.md similarity index 100% rename from .changeset/rank-colored-discord-embeds.md rename to .changeset/pre/rank-colored-discord-embeds.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 158536cb6..7664a898b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 4.1.1-beta.0 + +### Patch Changes + +- Use the player's rank color for Discord component embeds, preferring the plus color and falling back to the default preview color when no valid rank color is available. _[`c027c31`](https://github.com/SkyCryptWebsite/SkyCrypt-Frontend/commit/c027c31cc48c2f260004d4d0f7dc032063c3af25) [@DarthGigi](https://github.com/DarthGigi)_ + ## 4.1.0 ### Minor Changes diff --git a/package.json b/package.json index 6a273c7f3..a54181b8c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skycrypt-frontend", - "version": "4.1.0", + "version": "4.1.1-beta.0", "private": true, "license": "AGPL-3.0-only", "repository": { From 6aa012623c8ff8cb41b0dfdb2d4196560ac8bd11 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 20 Sep 2026 22:05:01 +0000 Subject: [PATCH 014/135] chore: version packages (stable) [skip ci] --- .changeset/pre.json | 4 ---- .changeset/pre/rank-colored-discord-embeds.md | 5 ----- CHANGELOG.md | 6 ++++++ package.json | 2 +- 4 files changed, 7 insertions(+), 10 deletions(-) delete mode 100644 .changeset/pre.json delete mode 100644 .changeset/pre/rank-colored-discord-embeds.md diff --git a/.changeset/pre.json b/.changeset/pre.json deleted file mode 100644 index f6799931c..000000000 --- a/.changeset/pre.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mode": "pre", - "tag": "beta" -} diff --git a/.changeset/pre/rank-colored-discord-embeds.md b/.changeset/pre/rank-colored-discord-embeds.md deleted file mode 100644 index e6df4b0e5..000000000 --- a/.changeset/pre/rank-colored-discord-embeds.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"skycrypt-frontend": patch ---- - -Use the player's rank color for Discord component embeds, preferring the plus color and falling back to the default preview color when no valid rank color is available. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7664a898b..e3f8fa7a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 4.1.1 + +### Patch Changes + +- Use the player's rank color for Discord component embeds, preferring the plus color and falling back to the default preview color when no valid rank color is available. _[`#349`](https://github.com/SkyCryptWebsite/SkyCrypt-Frontend/pull/349) [`c027c31`](https://github.com/SkyCryptWebsite/SkyCrypt-Frontend/commit/c027c31cc48c2f260004d4d0f7dc032063c3af25) [@DarthGigi](https://github.com/DarthGigi)_ + ## 4.1.1-beta.0 ### Patch Changes diff --git a/package.json b/package.json index a54181b8c..bc3473be3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skycrypt-frontend", - "version": "4.1.1-beta.0", + "version": "4.1.1", "private": true, "license": "AGPL-3.0-only", "repository": { From 72ab679782e8b02e5f05a40147462cd2aa1b2727 Mon Sep 17 00:00:00 2001 From: Gigi <47110839+DarthGigi@users.noreply.github.com> Date: Wed, 30 Sep 2026 10:32:59 +0200 Subject: [PATCH 015/135] Fix capitalization in project title in README Signed-off-by: Gigi <47110839+DarthGigi@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2b19b8c0e..5f540e3a2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ SkyCrypt 🍣

-

A Hypixel Skyblock Profile Viewer

+

A Hypixel SkyBlock Profile Viewer

Sponsor From f9541c54be4d9c8178610d27a5ce1471b9342eda Mon Sep 17 00:00:00 2001 From: Gigi <47110839+DarthGigi@users.noreply.github.com> Date: Wed, 30 Sep 2026 10:40:22 +0200 Subject: [PATCH 016/135] feat: add discord link to the discord embed --- src/lib/shared/discordEmbed.spec.ts | 4 ++++ src/lib/shared/discordEmbed.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/lib/shared/discordEmbed.spec.ts b/src/lib/shared/discordEmbed.spec.ts index f8342f5b3..d6c8ef3a1 100644 --- a/src/lib/shared/discordEmbed.spec.ts +++ b/src/lib/shared/discordEmbed.spec.ts @@ -1,5 +1,8 @@ import { expect, it } from "vitest"; import { createDiscordEmbed, serializeDiscordEmbed } from "./discordEmbed"; +import { env } from "$env/dynamic/public"; + +const { PUBLIC_DISCORD_INVITE } = env; it.each([ { rank: { plusColor: "#FF5555", rankColor: "#55FFFF" }, expected: 0xff5555 }, @@ -46,6 +49,7 @@ it("uses the profile data and puts a separator directly above valid link buttons label: "SkyCrypt", url: "https://sky.shiiyu.moe/stats/tiltedhoney/Kiwi?utm_source=SkyCrypt&utm_campaign=DiscordEmbed" }, + { type: 2, style: 5, label: "SkyCrypt Discord", url: PUBLIC_DISCORD_INVITE }, { type: 2, style: 5, diff --git a/src/lib/shared/discordEmbed.ts b/src/lib/shared/discordEmbed.ts index fc1fa0ae7..9b38eddef 100644 --- a/src/lib/shared/discordEmbed.ts +++ b/src/lib/shared/discordEmbed.ts @@ -1,6 +1,9 @@ import type { ModelsEmbedData } from "$lib/shared/api/orval-generated"; import { getLongDescription } from "$lib/shared/embedGenerator"; import type { APIContainerComponent } from "discord-api-types/v10"; +import { env } from "$env/dynamic/public"; + +const { PUBLIC_DISCORD_INVITE } = env; export function createDiscordEmbed(data: ModelsEmbedData) { const color = @@ -37,6 +40,7 @@ export function createDiscordEmbed(data: ModelsEmbedData) { type: 1, components: [ { type: 2, style: 5, label: "SkyCrypt", url: `${profileUrl}${trackingParams}` }, + { type: 2, style: 5, label: "SkyCrypt Discord", url: PUBLIC_DISCORD_INVITE }, { type: 2, style: 5, From 26565f962140c97bb04533ee5be8a1884c4e9516 Mon Sep 17 00:00:00 2001 From: Gigi <47110839+DarthGigi@users.noreply.github.com> Date: Wed, 30 Sep 2026 08:53:56 +0000 Subject: [PATCH 017/135] chore: oxfmt format --- src/lib/shared/discordEmbed.spec.ts | 2 +- src/lib/shared/discordEmbed.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/shared/discordEmbed.spec.ts b/src/lib/shared/discordEmbed.spec.ts index d6c8ef3a1..56128ba40 100644 --- a/src/lib/shared/discordEmbed.spec.ts +++ b/src/lib/shared/discordEmbed.spec.ts @@ -1,6 +1,6 @@ +import { env } from "$env/dynamic/public"; import { expect, it } from "vitest"; import { createDiscordEmbed, serializeDiscordEmbed } from "./discordEmbed"; -import { env } from "$env/dynamic/public"; const { PUBLIC_DISCORD_INVITE } = env; diff --git a/src/lib/shared/discordEmbed.ts b/src/lib/shared/discordEmbed.ts index 9b38eddef..944e83840 100644 --- a/src/lib/shared/discordEmbed.ts +++ b/src/lib/shared/discordEmbed.ts @@ -1,7 +1,7 @@ +import { env } from "$env/dynamic/public"; import type { ModelsEmbedData } from "$lib/shared/api/orval-generated"; import { getLongDescription } from "$lib/shared/embedGenerator"; import type { APIContainerComponent } from "discord-api-types/v10"; -import { env } from "$env/dynamic/public"; const { PUBLIC_DISCORD_INVITE } = env; From 9fdabace1d71cfdc3dbab5f2cf7a1c685ceec8e3 Mon Sep 17 00:00:00 2001 From: DarthGigi <47110839+DarthGigi@users.noreply.github.com> Date: Sat, 10 Oct 2026 02:02:39 +0200 Subject: [PATCH 018/135] feat: redesign the discord embeds --- .changeset/empty-dancers-dress.md | 5 + src/lib/shared/constants/game-modes.ts | 5 + src/lib/shared/discordEmbed.ts | 34 +++- src/lib/shared/embedGenerator.ts | 213 ++++++++++++------------- 4 files changed, 142 insertions(+), 115 deletions(-) create mode 100644 .changeset/empty-dancers-dress.md create mode 100644 src/lib/shared/constants/game-modes.ts diff --git a/.changeset/empty-dancers-dress.md b/.changeset/empty-dancers-dress.md new file mode 100644 index 000000000..85e3ce0e3 --- /dev/null +++ b/.changeset/empty-dancers-dress.md @@ -0,0 +1,5 @@ +--- +"skycrypt-frontend": minor +--- + +feat: redesign discord embeds diff --git a/src/lib/shared/constants/game-modes.ts b/src/lib/shared/constants/game-modes.ts new file mode 100644 index 000000000..ca3410f00 --- /dev/null +++ b/src/lib/shared/constants/game-modes.ts @@ -0,0 +1,5 @@ +export const gameModeIcons = { + ironman: "♲", + bingo: "Ⓑ", + island: "☀" +} as const; diff --git a/src/lib/shared/discordEmbed.ts b/src/lib/shared/discordEmbed.ts index 944e83840..cf9495011 100644 --- a/src/lib/shared/discordEmbed.ts +++ b/src/lib/shared/discordEmbed.ts @@ -1,6 +1,7 @@ import { env } from "$env/dynamic/public"; import type { ModelsEmbedData } from "$lib/shared/api/orval-generated"; import { getLongDescription } from "$lib/shared/embedGenerator"; +import { gameModeIcons } from "$src/lib/shared/constants/game-modes"; import type { APIContainerComponent } from "discord-api-types/v10"; const { PUBLIC_DISCORD_INVITE } = env; @@ -12,9 +13,14 @@ export function createDiscordEmbed(data: ModelsEmbedData) { const profile = encodeURIComponent(data.profile_cute_name || ""); const profileUrl = `https://sky.shiiyu.moe/stats/${username}/${profile}`; const trackingParams = "?utm_source=SkyCrypt&utm_campaign=DiscordEmbed"; - const title = `${data.displayName || data.username || "SkyCrypt"} (${data.profile_cute_name || "Selected profile"})` - .replace(/[\\[\]*_`<>]/g, "\\$&") - .replace(/[\r\n]/g, " "); + const rank = data.rank?.rankText ? ` [${data.rank.rankText}] ` : ""; + const gameModeIcon = gameModeIcons[data.game_mode as keyof typeof gameModeIcons]; + const profileName = data.profile_cute_name + ? ` (${data.profile_cute_name}${gameModeIcon ? " " + gameModeIcon : ""})` + : ""; + + const title = + rank + (data.displayName || data.username) + profileName.replace(/[\\[\]*_`<>]/g, "\\$&").replace(/[\r\n]/g, " "); const summary = { type: 10 as const, content: `### [${title}](${profileUrl})\n${getLongDescription(data).trim()}` @@ -39,19 +45,33 @@ export function createDiscordEmbed(data: ModelsEmbedData) { { type: 1, components: [ - { type: 2, style: 5, label: "SkyCrypt", url: `${profileUrl}${trackingParams}` }, - { type: 2, style: 5, label: "SkyCrypt Discord", url: PUBLIC_DISCORD_INVITE }, + { + type: 2, + style: 5, + label: "SkyCrypt", + url: `${profileUrl}${trackingParams}`, + emoji: { id: "1558259246070505512", name: "skycrypt" } + }, + { + type: 2, + style: 5, + label: "SkyCrypt Discord", + url: PUBLIC_DISCORD_INVITE, + emoji: { id: "1558259241679196280", name: "discord" } + }, { type: 2, style: 5, label: "Plancke", - url: `https://plancke.io/hypixel/player/stats/${username}${trackingParams}` + url: `https://plancke.io/hypixel/player/stats/${username}${trackingParams}`, + emoji: { id: "1558259244275597414", name: "plancke" } }, { type: 2, style: 5, label: "Elite", - url: `https://eliteskyblock.com/@${username}/${profile}${trackingParams}` + url: `https://eliteskyblock.com/@${username}/${profile}${trackingParams}`, + emoji: { id: "1558259242828435507", name: "elite" } } ] }, diff --git a/src/lib/shared/embedGenerator.ts b/src/lib/shared/embedGenerator.ts index 770a95e05..607340c6b 100644 --- a/src/lib/shared/embedGenerator.ts +++ b/src/lib/shared/embedGenerator.ts @@ -1,170 +1,167 @@ import type { ModelsEmbedData } from "$lib/shared/api/orval-generated"; import { formatNumber } from "$lib/shared/helper"; +import { gameModeIcons } from "$src/lib/shared/constants/game-modes"; import { formatDistanceToNowStrict } from "date-fns"; const skillEmojis = { - alchemy: "⚗️", - carpentry: "🪑", - combat: "⚔️", - enchanting: "🔮", - farming: "🌾", - fishing: "🎣", - foraging: "🌳", - mining: "⛏️", - runecrafting: "✨", - social: "💬", - taming: "🦴", - dungeons: "💀", - archer: "🏹", - berserk: "🗡️", - healer: "🚑", - mage: "🧙🏽", - tank: "🛡️" -} as Record; + combat: "<:combat:1558225211923636315>", + carpentry: "<:carpentry:1558225210753163354>", + berserk: "<:berserk:1558225207322476544>", + archer: "<:archer:1558225204704976966>", + alchemy: "<:alchemy:1558225203379703858>", + tank: "<:tank:1558225242030346390>", + taming: "<:taming:1558225240402690068>", + social: "<:social:1558225237189857442>", + runecrafting: "<:runecrafting:1558225233180237984>", + mining: "<:mining:1558225228235018271>", + hunting: "<:hunting:1558225223621283921>", + healer: "<:healer:1558225222702866442>", + foraging: "<:foraging:1558225221247303880>", + fishing: "<:fishing:1558225219745878036>", + farming: "<:farming:1558225218567147651>", + enchanting: "<:enchanting:1558225216164069436>", + dungeons: "<:dungeons:1558225214444150794>", + dungeoneering: "<:dungeoneering:1558225213223866399>", + mage: "<:mage:1558225227039637624>" +} as const; + +type SkillKey = keyof typeof skillEmojis; const slayerEmojis = { - zombie: "🧟", - spider: "🕸️", - wolf: "🐺", - enderman: "🔮", - blaze: "🔥", - vampire: "🩸" -} as Record; - -function getLongDescription(embedData: ModelsEmbedData) { - let output = ""; - - if (embedData == null || embedData.username == "") { - return output; + blaze: "<:blaze:1558225209226559529>", + zombie: "<:zombie:1558225246945939527>", + wolf: "<:wolf:1558225245532463225>", + vampire: "<:vampire:1558225244207063050>", + spider: "<:spider:1558225239299727380>", + enderman: "<:enderman:1558225217422102618>" +} as const; + +const miscEmojis = { + bank: "<:bank:1558225205909000202>", + slayyyter: "<:slayyyter:1558225235713593354>", + skills: "<:skills:1558225234547580948>", + crank: "<:crank:1558225232030994614>", + purse: "<:purse:1558225230613450814>", + networth: "<:networth:1558225229418074112>", + level: "<:level:1558225225768767530>" +} as const; + +function getLongDescription(embedData: ModelsEmbedData): string { + if (!embedData || !embedData.username) { + return ""; } - if (embedData.rank && embedData.rank.rankText) { - output += `🏅 Rank: ${embedData.rank.rankText + embedData.rank.plusText}\n`; - } + const lines: string[] = []; + // Profile Header if (embedData.skyblock_level) { - output += `🌟 Level: ${formatNumber(embedData.skyblock_level)}\n`; + lines.push(`${miscEmojis.level} Level: ${formatNumber(embedData.skyblock_level)}`); } - if (embedData.networth?.normal != null) { - output += `💸 Networth: ${formatNumber(embedData.networth.normal)}\n`; + // Financials + const hasFinancials = embedData.networth?.normal != null || embedData.purse != null || embedData.bank != null; + if (hasFinancials && lines.length > 0) { + lines.push(""); } - if (embedData.purse) { - output += `💰 Purse: ${formatNumber(embedData.purse)}\n`; + if (embedData.networth?.normal != null) { + lines.push(`${miscEmojis.networth} Networth: ${formatNumber(embedData.networth.normal)}`); } - if (embedData.bank) { - output += `🏦 Bank: ${formatNumber(embedData.bank)}\n`; + if (embedData.purse != null) { + lines.push(`${miscEmojis.purse} Purse: ${formatNumber(embedData.purse)}`); } - output += "\n"; + if (embedData.bank != null) { + lines.push(`${miscEmojis.bank} Bank: ${formatNumber(embedData.bank)}`); + } - const sortedSkills = [ - ["farming", "mining", "combat", "foraging", "taming", "carpentry"], - ["runecrafting", "social", "fishing", "enchanting", "alchemy"] - ]; + // Skills const skills = embedData.skills?.skills; if (embedData.skills && skills && Object.keys(skills).length > 0) { - output += `📚 Skills: ${embedData.skills.skillAverage}\n`; + lines.push(`\n${miscEmojis.skills} **Skills: ${embedData.skills.skillAverage}**`); + lines.push("-# ~~ ~~"); + + const sortedSkills: SkillKey[][] = [ + ["combat", "farming", "fishing", "mining", "foraging", "enchanting", "alchemy"], + ["carpentry", "runecrafting", "taming", "social", "hunting", "dungeoneering"] + ]; for (const skillGroup of sortedSkills) { - for (const skill of skillGroup) { - const data = skills[skill as keyof typeof skills]; - if (data == null) { - continue; - } + const row = skillGroup + .filter((skill) => skills[skill as keyof typeof skills] != null) + .map((skill) => `${skillEmojis[skill]} ${skills[skill as keyof typeof skills]}`) + .join(" ︲ "); - output += `${skillEmojis[skill]} ${data} `; + if (row.length > 0) { + lines.push(row); } - - output += "\n"; } - - output += "\n"; } - if (embedData.dungeons) { + // Dungeons + if (embedData.dungeons && embedData.dungeons.classAverage && embedData.dungeons.classAverage) { const classAverage = embedData.dungeons.classAverage; if (classAverage) { - output += `🪦 Dungeons: ${classAverage}\n`; + lines.push(`\n${skillEmojis.dungeons} **Dungeons: ${classAverage}**`); + lines.push("-# ~~ ~~"); } - output += `${skillEmojis["dungeons"]} ${embedData.dungeons.dungeoneering ?? 0} `; + const dungeonEntries: string[] = [`${skillEmojis.dungeoneering} ${embedData.dungeons.dungeoneering ?? 0}`]; + const classes = embedData.dungeons.classes; - if (classes != null) { - for (const [dclass, data] of Object.entries(classes)) { - output += `${skillEmojis[dclass]} ${data ?? 0} `; + if (classes) { + for (const [dclass, level] of Object.entries(classes)) { + const emoji = skillEmojis[dclass as SkillKey]; + if (emoji) { + dungeonEntries.push(`${emoji} ${level ?? 0}`); + } } } - output += "\n"; + lines.push(dungeonEntries.join(" ︲ ")); } - output += "\n"; - + // Slayer if (embedData.slayers && embedData.slayers.xp != null && embedData.slayers.xp > 0) { - output += `🤺 Slayer: ${formatNumber(embedData.slayers.xp)}\n`; + lines.push(`\n${miscEmojis.slayyyter} **Slayer: ${formatNumber(embedData.slayers.xp)}**`); + lines.push("-# ~~ ~~"); if (embedData.slayers.slayers) { const slayerOrder = ["zombie", "spider", "wolf", "enderman", "vampire", "blaze"] as const; - for (const slayer of slayerOrder) { - if (!embedData.slayers.slayers[slayer]) { - continue; - } + const items: string[] = []; - const slayerLevel = embedData.slayers.slayers[slayer]; - if (!slayerLevel) { - continue; + for (const slayer of slayerOrder) { + const level = embedData.slayers.slayers[slayer]; + if (level) { + items.push(`${slayerEmojis[slayer]} ${level}`); } + } - output += `${slayerEmojis[slayer]} ${slayerLevel} `; + if (items.length > 0) { + lines.push(items.join(" ︲ ")); } } - - output += "\n"; } - return output; + return lines.join("\n").trim() + "\n"; } -function getMetaTitle(embedData: ModelsEmbedData) { - if (embedData == null || embedData.username == "") { +function getMetaTitle(embedData: ModelsEmbedData): string { + if (!embedData || !embedData.username) { return ""; } - let metaTitle = embedData.displayName; - - switch (embedData.game_mode) { - case "ironman": - metaTitle += ` (${embedData.profile_cute_name} ♻️)`; - break; - - case "bingo": - metaTitle += ` (${embedData.profile_cute_name} 🎲)`; - break; - - case "island": - metaTitle += ` (${embedData.profile_cute_name} 🌴)`; - break; - - default: - metaTitle += ` (${embedData.profile_cute_name})`; - break; - } - - return metaTitle; + const gameModeIcon = gameModeIcons[embedData.game_mode as keyof typeof gameModeIcons] ?? ""; + return `${embedData.displayName} (${embedData.profile_cute_name} ${gameModeIcon})`.trim(); } -function getShortDescription(embedData: ModelsEmbedData) { - let description = ""; - - // Base - if (embedData.joined) { - description += `${embedData.displayName} has been playing SkyBlock for ${formatDistanceToNowStrict(embedData.joined)}`; +function getShortDescription(embedData: ModelsEmbedData): string { + if (!embedData.joined) { + return ""; } - return description; + return `${embedData.displayName} has been playing SkyBlock for ${formatDistanceToNowStrict(new Date(embedData.joined))}`; } export { getLongDescription, getMetaTitle, getShortDescription }; From 5f2d6732e132bfc61a6f7972f5c4b471018b90a8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 10 Oct 2026 00:03:26 +0000 Subject: [PATCH 019/135] chore: version packages (beta) [skip ci] --- .changeset/pre.json | 4 ++++ .changeset/{ => pre}/empty-dancers-dress.md | 0 CHANGELOG.md | 6 ++++++ package.json | 2 +- 4 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 .changeset/pre.json rename .changeset/{ => pre}/empty-dancers-dress.md (100%) diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 000000000..f6799931c --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,4 @@ +{ + "mode": "pre", + "tag": "beta" +} diff --git a/.changeset/empty-dancers-dress.md b/.changeset/pre/empty-dancers-dress.md similarity index 100% rename from .changeset/empty-dancers-dress.md rename to .changeset/pre/empty-dancers-dress.md diff --git a/CHANGELOG.md b/CHANGELOG.md index e3f8fa7a6..eca3d2dac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 4.2.0-beta.0 + +### Minor Changes + +- Feat: redesign discord embeds _[`9fdabac`](https://github.com/SkyCryptWebsite/SkyCrypt-Frontend/commit/9fdabace1d71cfdc3dbab5f2cf7a1c685ceec8e3) [@DarthGigi](https://github.com/DarthGigi)_ + ## 4.1.1 ### Patch Changes diff --git a/package.json b/package.json index bc3473be3..7615f2a32 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skycrypt-frontend", - "version": "4.1.1", + "version": "4.2.0-beta.0", "private": true, "license": "AGPL-3.0-only", "repository": { From 95927d14b4c01f763f852e36c5a4cb2a6ae0408b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 10 Oct 2026 00:08:10 +0000 Subject: [PATCH 020/135] chore: version packages (stable) [skip ci] --- .changeset/pre.json | 4 ---- .changeset/pre/empty-dancers-dress.md | 5 ----- CHANGELOG.md | 6 ++++++ package.json | 2 +- 4 files changed, 7 insertions(+), 10 deletions(-) delete mode 100644 .changeset/pre.json delete mode 100644 .changeset/pre/empty-dancers-dress.md diff --git a/.changeset/pre.json b/.changeset/pre.json deleted file mode 100644 index f6799931c..000000000 --- a/.changeset/pre.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "mode": "pre", - "tag": "beta" -} diff --git a/.changeset/pre/empty-dancers-dress.md b/.changeset/pre/empty-dancers-dress.md deleted file mode 100644 index 85e3ce0e3..000000000 --- a/.changeset/pre/empty-dancers-dress.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"skycrypt-frontend": minor ---- - -feat: redesign discord embeds diff --git a/CHANGELOG.md b/CHANGELOG.md index eca3d2dac..aa02f32ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 4.2.0 + +### Minor Changes + +- Feat: redesign discord embeds _[`#352`](https://github.com/SkyCryptWebsite/SkyCrypt-Frontend/pull/352) [`9fdabac`](https://github.com/SkyCryptWebsite/SkyCrypt-Frontend/commit/9fdabace1d71cfdc3dbab5f2cf7a1c685ceec8e3) [@DarthGigi](https://github.com/DarthGigi)_ + ## 4.2.0-beta.0 ### Minor Changes diff --git a/package.json b/package.json index 7615f2a32..77a997ec5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skycrypt-frontend", - "version": "4.2.0-beta.0", + "version": "4.2.0", "private": true, "license": "AGPL-3.0-only", "repository": { From c5c63e4c36d48c99c8183867b4a2d578d34cfe77 Mon Sep 17 00:00:00 2001 From: DarthGigi <47110839+DarthGigi@users.noreply.github.com> Date: Sat, 10 Oct 2026 02:11:09 +0200 Subject: [PATCH 021/135] fix: make skills even --- src/lib/shared/embedGenerator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/shared/embedGenerator.ts b/src/lib/shared/embedGenerator.ts index 607340c6b..f2e5caa6c 100644 --- a/src/lib/shared/embedGenerator.ts +++ b/src/lib/shared/embedGenerator.ts @@ -83,8 +83,8 @@ function getLongDescription(embedData: ModelsEmbedData): string { lines.push("-# ~~ ~~"); const sortedSkills: SkillKey[][] = [ - ["combat", "farming", "fishing", "mining", "foraging", "enchanting", "alchemy"], - ["carpentry", "runecrafting", "taming", "social", "hunting", "dungeoneering"] + ["combat", "farming", "fishing", "mining", "foraging", "enchanting"], + ["alchemy", "carpentry", "runecrafting", "taming", "social", "hunting"] ]; for (const skillGroup of sortedSkills) { From a020899a4071b4a7cf9607780878da4e7c59b866 Mon Sep 17 00:00:00 2001 From: DarthGigi <47110839+DarthGigi@users.noreply.github.com> Date: Sat, 10 Oct 2026 11:28:03 +0200 Subject: [PATCH 022/135] fix: use correct dungeon classes order --- src/lib/shared/embedGenerator.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/lib/shared/embedGenerator.ts b/src/lib/shared/embedGenerator.ts index f2e5caa6c..d5259cd5f 100644 --- a/src/lib/shared/embedGenerator.ts +++ b/src/lib/shared/embedGenerator.ts @@ -82,12 +82,12 @@ function getLongDescription(embedData: ModelsEmbedData): string { lines.push(`\n${miscEmojis.skills} **Skills: ${embedData.skills.skillAverage}**`); lines.push("-# ~~ ~~"); - const sortedSkills: SkillKey[][] = [ + const skillsOrder: SkillKey[][] = [ ["combat", "farming", "fishing", "mining", "foraging", "enchanting"], ["alchemy", "carpentry", "runecrafting", "taming", "social", "hunting"] ]; - for (const skillGroup of sortedSkills) { + for (const skillGroup of skillsOrder) { const row = skillGroup .filter((skill) => skills[skill as keyof typeof skills] != null) .map((skill) => `${skillEmojis[skill]} ${skills[skill as keyof typeof skills]}`) @@ -110,11 +110,13 @@ function getLongDescription(embedData: ModelsEmbedData): string { const dungeonEntries: string[] = [`${skillEmojis.dungeoneering} ${embedData.dungeons.dungeoneering ?? 0}`]; const classes = embedData.dungeons.classes; + const classOrder: SkillKey[] = ["healer", "mage", "berserk", "archer", "tank"]; if (classes) { - for (const [dclass, level] of Object.entries(classes)) { - const emoji = skillEmojis[dclass as SkillKey]; - if (emoji) { - dungeonEntries.push(`${emoji} ${level ?? 0}`); + for (const dclass of classOrder) { + const level = classes[dclass as keyof typeof classes]; + if (level != null) { + const emoji = skillEmojis[dclass]; + dungeonEntries.push(`${emoji} ${level}`); } } } From 9283efef19215d2479145b2125126059dab8ee83 Mon Sep 17 00:00:00 2001 From: DarthGigi <47110839+DarthGigi@users.noreply.github.com> Date: Sat, 10 Oct 2026 14:56:45 +0200 Subject: [PATCH 023/135] fix: update embedGenerator tests for username handling and emoji formatting --- src/lib/shared/discordEmbed.spec.ts | 25 ++- src/lib/shared/embedGenerator.spec.ts | 262 +++++++++++++++++++++----- 2 files changed, 237 insertions(+), 50 deletions(-) diff --git a/src/lib/shared/discordEmbed.spec.ts b/src/lib/shared/discordEmbed.spec.ts index 56128ba40..243ea415b 100644 --- a/src/lib/shared/discordEmbed.spec.ts +++ b/src/lib/shared/discordEmbed.spec.ts @@ -34,11 +34,15 @@ it("uses the profile data and puts a separator directly above valid link buttons expect(section).toMatchObject({ type: 9, components: [ - { content: expect.stringContaining("### [tiltedhoney (Kiwi)](https://sky.shiiyu.moe/stats/tiltedhoney/Kiwi)") } + { + content: expect.stringContaining( + "### [ [MVP] tiltedhoney (Kiwi)](https://sky.shiiyu.moe/stats/tiltedhoney/Kiwi)" + ) + } ], accessory: { type: 11, media: { url: "https://nmsr.nickac.dev/bust/debb9e1f6dcb4f5b83c291fd5388bb56?y=-20" } } }); - expect(JSON.stringify(section)).toContain("🌟 Level: 620.16"); + expect(JSON.stringify(section)).toContain("<:level:1558225225768767530> Level: 620.16"); expect(separator).toEqual({ type: 14, divider: true, spacing: 1 }); expect(row).toEqual({ type: 1, @@ -47,20 +51,29 @@ it("uses the profile data and puts a separator directly above valid link buttons type: 2, style: 5, label: "SkyCrypt", - url: "https://sky.shiiyu.moe/stats/tiltedhoney/Kiwi?utm_source=SkyCrypt&utm_campaign=DiscordEmbed" + url: "https://sky.shiiyu.moe/stats/tiltedhoney/Kiwi?utm_source=SkyCrypt&utm_campaign=DiscordEmbed", + emoji: { id: "1558259246070505512", name: "skycrypt" } + }, + { + type: 2, + style: 5, + label: "SkyCrypt Discord", + url: PUBLIC_DISCORD_INVITE, + emoji: { id: "1558259241679196280", name: "discord" } }, - { type: 2, style: 5, label: "SkyCrypt Discord", url: PUBLIC_DISCORD_INVITE }, { type: 2, style: 5, label: "Plancke", - url: "https://plancke.io/hypixel/player/stats/tiltedhoney?utm_source=SkyCrypt&utm_campaign=DiscordEmbed" + url: "https://plancke.io/hypixel/player/stats/tiltedhoney?utm_source=SkyCrypt&utm_campaign=DiscordEmbed", + emoji: { id: "1558259244275597414", name: "plancke" } }, { type: 2, style: 5, label: "Elite", - url: "https://eliteskyblock.com/@tiltedhoney/Kiwi?utm_source=SkyCrypt&utm_campaign=DiscordEmbed" + url: "https://eliteskyblock.com/@tiltedhoney/Kiwi?utm_source=SkyCrypt&utm_campaign=DiscordEmbed", + emoji: { id: "1558259242828435507", name: "elite" } } ] }); diff --git a/src/lib/shared/embedGenerator.spec.ts b/src/lib/shared/embedGenerator.spec.ts index 11a063ec8..6520681cc 100644 --- a/src/lib/shared/embedGenerator.spec.ts +++ b/src/lib/shared/embedGenerator.spec.ts @@ -4,73 +4,114 @@ import { afterEach, describe, it, vi } from "vitest"; describe.concurrent("embedGenerator", () => { describe.concurrent("getMetaTitle", () => { + it("should return empty string when embedData is null", ({ expect }) => { + expect.assertions(1); + // @ts-expect-error Testing null case + const result = getMetaTitle(null); + expect(result).toBe(""); + }); + + it("should return empty string when embedData is undefined", ({ expect }) => { + expect.assertions(1); + // @ts-expect-error Testing undefined case + const result = getMetaTitle(undefined); + expect(result).toBe(""); + }); + + it("should return empty string when username is missing", ({ expect }) => { + expect.assertions(1); + const embedData: ModelsEmbedData = { + displayName: "TestPlayer" + }; + + const result = getMetaTitle(embedData); + expect(result).toBe(""); + }); + + it("should return empty string when username is empty string", ({ expect }) => { + expect.assertions(1); + const embedData: ModelsEmbedData = { + username: "", + displayName: "TestPlayer" + }; + + const result = getMetaTitle(embedData); + expect(result).toBe(""); + }); + it("should return title without profile name when missing", ({ expect }) => { expect.assertions(1); const embedData: ModelsEmbedData = { + username: "TestPlayer", displayName: "TestPlayer" }; const result = getMetaTitle(embedData); - expect(result).toBe("TestPlayer (undefined)"); + expect(result).toBe("TestPlayer (undefined )"); }); it("should return title with profile name and no icon for default game mode", ({ expect }) => { expect.assertions(1); const embedData: ModelsEmbedData = { + username: "TestPlayer", displayName: "TestPlayer", profile_cute_name: "Banana" }; const result = getMetaTitle(embedData); - expect(result).toBe("TestPlayer (Banana)"); + expect(result).toBe("TestPlayer (Banana )"); }); it("should return title with ironman icon for ironman game mode", ({ expect }) => { expect.assertions(1); const embedData: ModelsEmbedData = { + username: "IronPlayer", displayName: "IronPlayer", profile_cute_name: "Iron Profile", game_mode: "ironman" }; const result = getMetaTitle(embedData); - expect(result).toBe("IronPlayer (Iron Profile ♻️)"); + expect(result).toBe("IronPlayer (Iron Profile ♲)"); }); it("should return title with bingo icon for bingo game mode", ({ expect }) => { expect.assertions(1); const embedData: ModelsEmbedData = { + username: "BingoPlayer", displayName: "BingoPlayer", profile_cute_name: "Bingo Profile", game_mode: "bingo" }; const result = getMetaTitle(embedData); - expect(result).toBe("BingoPlayer (Bingo Profile 🎲)"); + expect(result).toBe("BingoPlayer (Bingo Profile Ⓑ)"); }); it("should return title with island icon for island game mode", ({ expect }) => { expect.assertions(1); const embedData: ModelsEmbedData = { + username: "IslandPlayer", displayName: "IslandPlayer", profile_cute_name: "Island Profile", game_mode: "island" }; const result = getMetaTitle(embedData); - expect(result).toBe("IslandPlayer (Island Profile 🌴)"); + expect(result).toBe("IslandPlayer (Island Profile ☀)"); }); it("should handle unknown game mode as default", ({ expect }) => { expect.assertions(1); const embedData: ModelsEmbedData = { + username: "UnknownPlayer", displayName: "UnknownPlayer", profile_cute_name: "Custom Profile", game_mode: "unknown_mode" }; const result = getMetaTitle(embedData); - expect(result).toBe("UnknownPlayer (Custom Profile)"); + expect(result).toBe("UnknownPlayer (Custom Profile )"); }); }); @@ -89,67 +130,94 @@ describe.concurrent("embedGenerator", () => { expect(result).toBe(""); }); + it("should return empty string when username is missing", ({ expect }) => { + expect.assertions(1); + const embedData: ModelsEmbedData = { + skyblock_level: 250.5 + }; + + const result = getLongDescription(embedData); + expect(result).toBe(""); + }); + + it("should return empty string when username is empty string", ({ expect }) => { + expect.assertions(1); + const embedData: ModelsEmbedData = { + username: "", + skyblock_level: 250.5 + }; + + const result = getLongDescription(embedData); + expect(result).toBe(""); + }); + it("should format skyblock level with emoji", ({ expect }) => { expect.assertions(2); const embedData: ModelsEmbedData = { + username: "TestPlayer", skyblock_level: 250.5 }; const result = getLongDescription(embedData); - expect(result).toContain("🌟 Level:"); + expect(result).toContain("<:level:1558225225768767530> Level:"); expect(result).toContain("250.5"); }); it("should format networth with emoji when normal value exists", ({ expect }) => { expect.assertions(2); const embedData: ModelsEmbedData = { + username: "TestPlayer", networth: { normal: 1500000000 } }; const result = getLongDescription(embedData); - expect(result).toContain("💸 Networth:"); + expect(result).toContain("<:networth:1558225229418074112> Networth:"); expect(result).toContain("1.50B"); }); it("should not include networth when normal value is null", ({ expect }) => { expect.assertions(1); const embedData: ModelsEmbedData = { + username: "TestPlayer", networth: { normal: undefined } }; const result = getLongDescription(embedData); - expect(result).not.toContain("💸 Networth:"); + expect(result).not.toContain("<:networth:1558225229418074112> Networth:"); }); it("should format purse with emoji", ({ expect }) => { expect.assertions(2); const embedData: ModelsEmbedData = { + username: "TestPlayer", purse: 50000000 }; const result = getLongDescription(embedData); - expect(result).toContain("💰 Purse:"); + expect(result).toContain("<:purse:1558225230613450814> Purse:"); expect(result).toContain("50M"); }); it("should format bank with emoji", ({ expect }) => { expect.assertions(2); const embedData: ModelsEmbedData = { + username: "TestPlayer", bank: 100000000 }; const result = getLongDescription(embedData); - expect(result).toContain("🏦 Bank:"); + expect(result).toContain("<:bank:1558225205909000202> Bank:"); expect(result).toContain("100M"); }); it("should format skills with emojis and skill average", ({ expect }) => { expect.assertions(5); const embedData: ModelsEmbedData = { + username: "TestPlayer", skills: { skillAverage: 45.5, skills: { @@ -162,16 +230,42 @@ describe.concurrent("embedGenerator", () => { }; const result = getLongDescription(embedData); - expect(result).toContain("📚 Skills: 45.5"); - expect(result).toContain("🌾 50"); - expect(result).toContain("⛏️ 60"); - expect(result).toContain("⚔️ 50"); - expect(result).toContain("🌳 50"); + expect(result).toContain("<:skills:1558225234547580948> **Skills: 45.5**"); + expect(result).toContain("<:farming:1558225218567147651> 50"); + expect(result).toContain("<:mining:1558225228235018271> 60"); + expect(result).toContain("<:combat:1558225211923636315> 50"); + expect(result).toContain("<:foraging:1558225221247303880> 50"); + }); + + it("should order skills correctly across groups", ({ expect }) => { + expect.assertions(2); + const embedData: ModelsEmbedData = { + username: "TestPlayer", + skills: { + skillAverage: 50, + skills: { + enchanting: 60, + combat: 50, + farming: 40, + hunting: 30, + alchemy: 20 + } + } + }; + + const result = getLongDescription(embedData); + // Group 1: combat, farming, enchanting + expect(result).toContain( + "<:combat:1558225211923636315> 50 ︲ <:farming:1558225218567147651> 40 ︲ <:enchanting:1558225216164069436> 60" + ); + // Group 2: alchemy, hunting + expect(result).toContain("<:alchemy:1558225203379703858> 20 ︲ <:hunting:1558225223621283921> 30"); }); it("should not include skills when skills object is empty", ({ expect }) => { expect.assertions(1); const embedData: ModelsEmbedData = { + username: "TestPlayer", skills: { skillAverage: 0, skills: {} @@ -179,12 +273,13 @@ describe.concurrent("embedGenerator", () => { }; const result = getLongDescription(embedData); - expect(result).not.toContain("📚 Skills:"); + expect(result).not.toContain("<:skills:1558225234547580948> **Skills:"); }); it("should skip null skills in skills object", ({ expect }) => { expect.assertions(2); const embedData: ModelsEmbedData = { + username: "TestPlayer", skills: { skillAverage: 25, skills: { @@ -195,13 +290,14 @@ describe.concurrent("embedGenerator", () => { }; const result = getLongDescription(embedData); - expect(result).toContain("🌾 50"); - expect(result).toContain("⚔️ 30"); + expect(result).toContain("<:farming:1558225218567147651> 50"); + expect(result).toContain("<:combat:1558225211923636315> 30"); }); it("should format dungeons with class average and dungeoneering level", ({ expect }) => { expect.assertions(3); const embedData: ModelsEmbedData = { + username: "TestPlayer", dungeons: { classAverage: 40.5, dungeoneering: 50, @@ -216,27 +312,46 @@ describe.concurrent("embedGenerator", () => { }; const result = getLongDescription(embedData); - expect(result).toContain("🪦 Dungeons: 40.5"); - expect(result).toContain("💀 50"); - expect(result).toContain("🧙🏽 50"); + expect(result).toContain("<:dungeons:1558225214444150794> **Dungeons: 40.5**"); + expect(result).toContain("<:dungeoneering:1558225213223866399> 50"); + expect(result).toContain("<:mage:1558225227039637624> 50"); }); it("should default dungeoneering to 0 when undefined", ({ expect }) => { expect.assertions(1); const embedData: ModelsEmbedData = { + username: "TestPlayer", dungeons: { classAverage: 20 } }; const result = getLongDescription(embedData); - expect(result).toContain("💀 0"); + expect(result).toContain("<:dungeoneering:1558225213223866399> 0"); + }); + + it("should not include dungeons when classAverage is undefined", ({ expect }) => { + expect.assertions(1); + const embedData: ModelsEmbedData = { + username: "TestPlayer", + dungeons: { + dungeoneering: 30, + classes: { + mage: 40 + } + } + }; + + const result = getLongDescription(embedData); + expect(result).not.toContain("<:dungeons:1558225214444150794>"); }); - it("should default class values to 0 when undefined", ({ expect }) => { + it("should format class values when class average is present", ({ expect }) => { expect.assertions(1); const embedData: ModelsEmbedData = { + username: "TestPlayer", dungeons: { + classAverage: 40, dungeoneering: 30, classes: { mage: 40 @@ -245,12 +360,36 @@ describe.concurrent("embedGenerator", () => { }; const result = getLongDescription(embedData); - expect(result).toContain("🧙🏽 40"); + expect(result).toContain("<:mage:1558225227039637624> 40"); + }); + + it("should order dungeon classes correctly according to healer, mage, berserk, archer, tank", ({ expect }) => { + expect.assertions(1); + const embedData: ModelsEmbedData = { + username: "TestPlayer", + dungeons: { + classAverage: 40, + dungeoneering: 50, + classes: { + tank: 30, + mage: 50, + archer: 35, + healer: 45, + berserk: 40 + } + } + }; + + const result = getLongDescription(embedData); + expect(result).toContain( + "<:dungeoneering:1558225213223866399> 50 ︲ <:healer:1558225222702866442> 45 ︲ <:mage:1558225227039637624> 50 ︲ <:berserk:1558225207322476544> 40 ︲ <:archer:1558225204704976966> 35 ︲ <:tank:1558225242030346390> 30" + ); }); it("should format slayers with total xp and individual slayer levels", ({ expect }) => { expect.assertions(4); const embedData: ModelsEmbedData = { + username: "TestPlayer", slayers: { xp: 5000000, slayers: { @@ -263,15 +402,37 @@ describe.concurrent("embedGenerator", () => { }; const result = getLongDescription(embedData); - expect(result).toContain("🤺 Slayer: 5M"); - expect(result).toContain("🧟 9"); - expect(result).toContain("🕸️ 8"); - expect(result).toContain("🐺 8"); + expect(result).toContain("<:slayyyter:1558225235713593354> **Slayer: 5M**"); + expect(result).toContain("<:zombie:1558225246945939527> 9"); + expect(result).toContain("<:spider:1558225239299727380> 8"); + expect(result).toContain("<:wolf:1558225245532463225> 8"); + }); + + it("should order slayers correctly", ({ expect }) => { + expect.assertions(1); + const embedData: ModelsEmbedData = { + username: "TestPlayer", + slayers: { + xp: 1000000, + slayers: { + blaze: 3, + zombie: 9, + wolf: 7, + spider: 8 + } + } + }; + + const result = getLongDescription(embedData); + expect(result).toContain( + "<:zombie:1558225246945939527> 9 ︲ <:spider:1558225239299727380> 8 ︲ <:wolf:1558225245532463225> 7 ︲ <:blaze:1558225209226559529> 3" + ); }); it("should not include slayers section when xp is 0", ({ expect }) => { expect.assertions(1); const embedData: ModelsEmbedData = { + username: "TestPlayer", slayers: { xp: 0, slayers: { @@ -281,12 +442,13 @@ describe.concurrent("embedGenerator", () => { }; const result = getLongDescription(embedData); - expect(result).not.toContain("🤺 Slayer:"); + expect(result).not.toContain("<:slayyyter:1558225235713593354> **Slayer:"); }); it("should not include slayers section when xp is undefined", ({ expect }) => { expect.assertions(1); const embedData: ModelsEmbedData = { + username: "TestPlayer", slayers: { slayers: { zombie: 5 @@ -295,12 +457,13 @@ describe.concurrent("embedGenerator", () => { }; const result = getLongDescription(embedData); - expect(result).not.toContain("🤺 Slayer:"); + expect(result).not.toContain("<:slayyyter:1558225235713593354> **Slayer:"); }); it("should skip slayers with undefined values", ({ expect }) => { expect.assertions(2); const embedData: ModelsEmbedData = { + username: "TestPlayer", slayers: { xp: 1000000, slayers: { @@ -311,13 +474,14 @@ describe.concurrent("embedGenerator", () => { }; const result = getLongDescription(embedData); - expect(result).toContain("🧟 7"); - expect(result).toContain("🐺 6"); + expect(result).toContain("<:zombie:1558225246945939527> 7"); + expect(result).toContain("<:wolf:1558225245532463225> 6"); }); it("should handle complete embedData with all fields", ({ expect }) => { expect.assertions(10); const embedData: ModelsEmbedData = { + username: "TestPlayer", skyblock_level: 300, networth: { normal: 2000000000 @@ -349,16 +513,16 @@ describe.concurrent("embedGenerator", () => { }; const result = getLongDescription(embedData); - expect(result).toContain("🌟 Level: 300"); - expect(result).toContain("💸 Networth: 2B"); - expect(result).toContain("💰 Purse: 75M"); - expect(result).toContain("🏦 Bank: 150M"); - expect(result).toContain("📚 Skills: 50"); - expect(result).toContain("🌾 60"); - expect(result).toContain("🪦 Dungeons: 45"); - expect(result).toContain("💀 50"); - expect(result).toContain("🤺 Slayer: 10M"); - expect(result).toContain("🧟 9"); + expect(result).toContain("<:level:1558225225768767530> Level: 300"); + expect(result).toContain("<:networth:1558225229418074112> Networth: 2B"); + expect(result).toContain("<:purse:1558225230613450814> Purse: 75M"); + expect(result).toContain("<:bank:1558225205909000202> Bank: 150M"); + expect(result).toContain("<:skills:1558225234547580948> **Skills: 50**"); + expect(result).toContain("<:farming:1558225218567147651> 60"); + expect(result).toContain("<:dungeons:1558225214444150794> **Dungeons: 45**"); + expect(result).toContain("<:dungeoneering:1558225213223866399> 50"); + expect(result).toContain("<:slayyyter:1558225235713593354> **Slayer: 10M**"); + expect(result).toContain("<:zombie:1558225246945939527> 9"); }); it("should handle embedData with all fields missing gracefully", ({ expect }) => { @@ -366,7 +530,17 @@ describe.concurrent("embedGenerator", () => { const embedData: ModelsEmbedData = {}; const result = getLongDescription(embedData); - expect(result).toBe("\n\n"); + expect(result).toBe(""); + }); + + it("should handle embedData with only username gracefully", ({ expect }) => { + expect.assertions(1); + const embedData: ModelsEmbedData = { + username: "TestPlayer" + }; + + const result = getLongDescription(embedData); + expect(result).toBe("\n"); }); }); From 2cc636beed032726b6f3f1042542461cd58ed531 Mon Sep 17 00:00:00 2001 From: DarthGigi <47110839+DarthGigi@users.noreply.github.com> Date: Sat, 10 Oct 2026 15:32:23 +0200 Subject: [PATCH 024/135] fix: update rank display to include plusText in Discord embed --- src/lib/shared/discordEmbed.spec.ts | 2 +- src/lib/shared/discordEmbed.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/shared/discordEmbed.spec.ts b/src/lib/shared/discordEmbed.spec.ts index 243ea415b..0ca328563 100644 --- a/src/lib/shared/discordEmbed.spec.ts +++ b/src/lib/shared/discordEmbed.spec.ts @@ -36,7 +36,7 @@ it("uses the profile data and puts a separator directly above valid link buttons components: [ { content: expect.stringContaining( - "### [ [MVP] tiltedhoney (Kiwi)](https://sky.shiiyu.moe/stats/tiltedhoney/Kiwi)" + "### [ [MVP+] tiltedhoney (Kiwi)](https://sky.shiiyu.moe/stats/tiltedhoney/Kiwi)" ) } ], diff --git a/src/lib/shared/discordEmbed.ts b/src/lib/shared/discordEmbed.ts index cf9495011..0973ecbab 100644 --- a/src/lib/shared/discordEmbed.ts +++ b/src/lib/shared/discordEmbed.ts @@ -13,7 +13,7 @@ export function createDiscordEmbed(data: ModelsEmbedData) { const profile = encodeURIComponent(data.profile_cute_name || ""); const profileUrl = `https://sky.shiiyu.moe/stats/${username}/${profile}`; const trackingParams = "?utm_source=SkyCrypt&utm_campaign=DiscordEmbed"; - const rank = data.rank?.rankText ? ` [${data.rank.rankText}] ` : ""; + const rank = data.rank?.rankText ? ` [${data.rank.rankText + data.rank.plusText}] ` : ""; const gameModeIcon = gameModeIcons[data.game_mode as keyof typeof gameModeIcons]; const profileName = data.profile_cute_name ? ` (${data.profile_cute_name}${gameModeIcon ? " " + gameModeIcon : ""})` From 28fed8091a35d78ed5f812c2a406774cc03790e9 Mon Sep 17 00:00:00 2001 From: DarthGigi <47110839+DarthGigi@users.noreply.github.com> Date: Sat, 10 Oct 2026 15:51:24 +0200 Subject: [PATCH 025/135] fix: update skill and class terminology in embed descriptions --- src/lib/shared/discordEmbed.spec.ts | 284 ++++++++++++++++++++------ src/lib/shared/embedGenerator.spec.ts | 18 +- src/lib/shared/embedGenerator.ts | 6 +- 3 files changed, 229 insertions(+), 79 deletions(-) diff --git a/src/lib/shared/discordEmbed.spec.ts b/src/lib/shared/discordEmbed.spec.ts index 0ca328563..c7a680a5c 100644 --- a/src/lib/shared/discordEmbed.spec.ts +++ b/src/lib/shared/discordEmbed.spec.ts @@ -1,81 +1,231 @@ import { env } from "$env/dynamic/public"; -import { expect, it } from "vitest"; +import type { ModelsEmbedData } from "$lib/shared/api/orval-generated"; +import type { APIBaseComponent, ComponentType } from "discord-api-types/v10"; +import { describe, expect, it } from "vitest"; import { createDiscordEmbed, serializeDiscordEmbed } from "./discordEmbed"; const { PUBLIC_DISCORD_INVITE } = env; -it.each([ - { rank: { plusColor: "#FF5555", rankColor: "#55FFFF" }, expected: 0xff5555 }, - { rank: { rankColor: "#55ffff" }, expected: 0x55ffff }, - { rank: { plusColor: "#000000", rankColor: "#55FFFF" }, expected: 0 }, - { rank: { plusColor: "invalid", rankColor: "#55FFFF" }, expected: 0x55ffff }, - { rank: { rankColor: "#12345678" }, expected: 0x282828 }, - { rank: undefined, expected: 0x282828 } -])("uses the preferred valid rank color: $rank", ({ rank, expected }) => { - expect(createDiscordEmbed({ username: "tiltedhoney", rank }).component.accent_color).toBe(expected); -}); - -it("keeps embedded JSON inside its script element", () => { - const embed = createDiscordEmbed({ username: "&" }); - const json = serializeDiscordEmbed(embed); - expect(json).not.toContain("<"); - expect(JSON.parse(json)).toEqual(embed); -}); +describe("discordEmbed", () => { + it.each([ + { rank: { plusColor: "#FF5555", rankColor: "#55FFFF" }, expected: 0xff5555 }, + { rank: { rankColor: "#55ffff" }, expected: 0x55ffff }, + { rank: { plusColor: "#000000", rankColor: "#55FFFF" }, expected: 0 }, + { rank: { plusColor: "invalid", rankColor: "#55FFFF" }, expected: 0x55ffff }, + { rank: { rankColor: "#12345678" }, expected: 0x282828 }, + { rank: undefined, expected: 0x282828 } + ])("uses the preferred valid rank color: $rank", ({ rank, expected }) => { + expect(createDiscordEmbed({ username: "tiltedhoney", rank }).component.accent_color).toBe(expected); + }); -it("uses the profile data and puts a separator directly above valid link buttons", () => { - const embed = createDiscordEmbed({ - username: "tiltedhoney", - profile_cute_name: "Kiwi", - uuid: "debb9e1f6dcb4f5b83c291fd5388bb56", - rank: { rankText: "MVP", plusText: "+" }, - skyblock_level: 620.16 + it("keeps embedded JSON inside its script element", () => { + const embed = createDiscordEmbed({ username: "&" }); + const json = serializeDiscordEmbed(embed); + expect(json).not.toContain("<"); + expect(JSON.parse(json)).toEqual(embed); }); - const [section, separator, row, footer] = embed.component.components; - expect(section).toMatchObject({ - type: 9, - components: [ - { - content: expect.stringContaining( - "### [ [MVP+] tiltedhoney (Kiwi)](https://sky.shiiyu.moe/stats/tiltedhoney/Kiwi)" - ) + + it("uses the profile data and puts a separator directly above valid link buttons", () => { + const embed = createDiscordEmbed({ + username: "tiltedhoney", + profile_cute_name: "Kiwi", + uuid: "debb9e1f6dcb4f5b83c291fd5388bb56", + rank: { rankText: "MVP", plusText: "+" }, + skyblock_level: 620.16 + }); + const [section, separator, row, footer] = embed.component.components; + expect(section).toMatchObject({ + type: 9, + components: [ + { + content: expect.stringContaining( + "### [ [MVP+] tiltedhoney (Kiwi)](https://sky.shiiyu.moe/stats/tiltedhoney/Kiwi)" + ) + } + ], + accessory: { + type: 11, + media: { url: "https://nmsr.nickac.dev/bust/debb9e1f6dcb4f5b83c291fd5388bb56?y=-20" } } - ], - accessory: { type: 11, media: { url: "https://nmsr.nickac.dev/bust/debb9e1f6dcb4f5b83c291fd5388bb56?y=-20" } } + }); + expect(JSON.stringify(section)).toContain("<:level:1558225225768767530> Level: 620.16"); + expect(separator).toEqual({ type: 14, divider: true, spacing: 1 }); + expect(row).toEqual({ + type: 1, + components: [ + { + type: 2, + style: 5, + label: "SkyCrypt", + url: "https://sky.shiiyu.moe/stats/tiltedhoney/Kiwi?utm_source=SkyCrypt&utm_campaign=DiscordEmbed", + emoji: { id: "1558259246070505512", name: "skycrypt" } + }, + { + type: 2, + style: 5, + label: "SkyCrypt Discord", + url: PUBLIC_DISCORD_INVITE, + emoji: { id: "1558259241679196280", name: "discord" } + }, + { + type: 2, + style: 5, + label: "Plancke", + url: "https://plancke.io/hypixel/player/stats/tiltedhoney?utm_source=SkyCrypt&utm_campaign=DiscordEmbed", + emoji: { id: "1558259244275597414", name: "plancke" } + }, + { + type: 2, + style: 5, + label: "Elite", + url: "https://eliteskyblock.com/@tiltedhoney/Kiwi?utm_source=SkyCrypt&utm_campaign=DiscordEmbed", + emoji: { id: "1558259242828435507", name: "elite" } + } + ] + }); + expect(footer).toEqual({ type: 10, content: `-# SkyCrypt • v${__NPM_PACKAGE_VERSION__}` }); }); - expect(JSON.stringify(section)).toContain("<:level:1558225225768767530> Level: 620.16"); - expect(separator).toEqual({ type: 14, divider: true, spacing: 1 }); - expect(row).toEqual({ - type: 1, - components: [ - { - type: 2, - style: 5, - label: "SkyCrypt", - url: "https://sky.shiiyu.moe/stats/tiltedhoney/Kiwi?utm_source=SkyCrypt&utm_campaign=DiscordEmbed", - emoji: { id: "1558259246070505512", name: "skycrypt" } - }, - { - type: 2, - style: 5, - label: "SkyCrypt Discord", - url: PUBLIC_DISCORD_INVITE, - emoji: { id: "1558259241679196280", name: "discord" } + + describe("Discord Component Embed requirements and limits", () => { + const fullMockData: ModelsEmbedData = { + username: "tiltedhoney", + displayName: "TiltedHoney", + profile_cute_name: "Kiwi", + game_mode: "ironman", + uuid: "debb9e1f6dcb4f5b83c291fd5388bb56", + rank: { rankText: "MVP", plusText: "++", rankColor: "#FFAA00", plusColor: "#FF5555" }, + skyblock_level: 420.69, + networth: { normal: 15000000000 }, + purse: 500000000, + bank: 1000000000, + skills: { + skillAverage: 55.5, + skills: { + combat: 60, + farming: 60, + fishing: 50, + mining: 60, + foraging: 50, + enchanting: 60, + alchemy: 50, + carpentry: 50, + runecrafting: 25, + taming: 50, + social: 20, + hunting: 25 + } }, - { - type: 2, - style: 5, - label: "Plancke", - url: "https://plancke.io/hypixel/player/stats/tiltedhoney?utm_source=SkyCrypt&utm_campaign=DiscordEmbed", - emoji: { id: "1558259244275597414", name: "plancke" } + dungeons: { + classAverage: 50, + dungeoneering: 50, + classes: { + healer: 50, + mage: 50, + berserk: 50, + archer: 50, + tank: 50 + } }, - { - type: 2, - style: 5, - label: "Elite", - url: "https://eliteskyblock.com/@tiltedhoney/Kiwi?utm_source=SkyCrypt&utm_campaign=DiscordEmbed", - emoji: { id: "1558259242828435507", name: "elite" } + slayers: { + xp: 15000000, + slayers: { + zombie: 9, + spider: 9, + wolf: 9, + enderman: 9, + vampire: 5, + blaze: 9 + } } - ] + }; + + it("does not exceed the Discord 3000 byte limit for serialized JSON with complete profile data", () => { + const embed = createDiscordEmbed(fullMockData); + const json = serializeDiscordEmbed(embed); + const byteLength = Buffer.byteLength(json, "utf-8"); + + expect(byteLength).toBeLessThanOrEqual(3000); + }); + + it("does not exceed the Discord 3000 byte limit without uuid (fallback layout)", () => { + const dataWithoutUuid = { ...fullMockData, uuid: undefined }; + const embed = createDiscordEmbed(dataWithoutUuid); + const json = serializeDiscordEmbed(embed); + const byteLength = Buffer.byteLength(json, "utf-8"); + + expect(byteLength).toBeLessThanOrEqual(3000); + }); + + it("has a single top-level Container (type 17) component", () => { + const embed = createDiscordEmbed(fullMockData); + expect(embed).toHaveProperty("component"); + expect(embed.component.type).toBe(17); + expect(Array.isArray(embed.component.components)).toBe(true); + }); + + it("does not exceed the 40 component limit in total", () => { + const embed = createDiscordEmbed(fullMockData); + + function countComponents(comp: unknown): number { + if (!comp || typeof comp !== "object") return 0; + let count = 1; + if ("components" in comp && Array.isArray(comp.components)) { + for (const child of comp.components) { + count += countComponents(child); + } + } + if ("accessory" in comp && comp.accessory) { + count += countComponents(comp.accessory); + } + return count; + } + + const totalComponents = countComponents(embed.component); + expect(totalComponents).toBeLessThanOrEqual(40); + }); + + it("only uses allowed component types and valid button structures", () => { + const embed = createDiscordEmbed(fullMockData); + const allowedTypes = [1, 2, 9, 10, 11, 12, 14, 17]; + const allowedButtonKeys = new Set(["id", "type", "url", "style", "label", "emoji", "disabled"]); + + function validateComponent(comp: APIBaseComponent & Record) { + expect(allowedTypes).toContain(comp.type); + + if (comp.type === 2) { + // Button must be link style (5) + expect(comp.style).toBe(5); + expect(typeof comp.url).toBe("string"); + expect((comp.url as string).length).toBeLessThanOrEqual(2048); + expect((comp.url as string).startsWith("http://") || (comp.url as string).startsWith("https://")).toBe(true); + expect(comp.label != null || comp.emoji != null).toBe(true); + + for (const key of Object.keys(comp)) { + expect(allowedButtonKeys.has(key)).toBe(true); + } + } + + if (comp.type === 11 && comp.media && typeof comp.media === "object") { + const media = comp.media as { url?: string }; + expect(typeof media.url).toBe("string"); + expect((media.url as string).length).toBeLessThanOrEqual(2048); + expect((media.url as string).startsWith("http://") || (media.url as string).startsWith("https://")).toBe( + true + ); + } + + if ("components" in comp && Array.isArray(comp.components)) { + for (const child of comp.components) { + validateComponent(child as APIBaseComponent & Record); + } + } + + if ("accessory" in comp && comp.accessory && typeof comp.accessory === "object") { + validateComponent(comp.accessory as APIBaseComponent & Record); + } + } + + validateComponent(embed.component as APIBaseComponent & Record); + }); }); - expect(footer).toEqual({ type: 10, content: `-# SkyCrypt • v${__NPM_PACKAGE_VERSION__}` }); }); diff --git a/src/lib/shared/embedGenerator.spec.ts b/src/lib/shared/embedGenerator.spec.ts index 6520681cc..e6ca692b5 100644 --- a/src/lib/shared/embedGenerator.spec.ts +++ b/src/lib/shared/embedGenerator.spec.ts @@ -230,7 +230,7 @@ describe.concurrent("embedGenerator", () => { }; const result = getLongDescription(embedData); - expect(result).toContain("<:skills:1558225234547580948> **Skills: 45.5**"); + expect(result).toContain("<:skills:1558225234547580948> **Skill Average: 45.5**"); expect(result).toContain("<:farming:1558225218567147651> 50"); expect(result).toContain("<:mining:1558225228235018271> 60"); expect(result).toContain("<:combat:1558225211923636315> 50"); @@ -273,7 +273,7 @@ describe.concurrent("embedGenerator", () => { }; const result = getLongDescription(embedData); - expect(result).not.toContain("<:skills:1558225234547580948> **Skills:"); + expect(result).not.toContain("<:skills:1558225234547580948> **Skill Average:"); }); it("should skip null skills in skills object", ({ expect }) => { @@ -312,7 +312,7 @@ describe.concurrent("embedGenerator", () => { }; const result = getLongDescription(embedData); - expect(result).toContain("<:dungeons:1558225214444150794> **Dungeons: 40.5**"); + expect(result).toContain("<:dungeons:1558225214444150794> **Class Average: 40.5**"); expect(result).toContain("<:dungeoneering:1558225213223866399> 50"); expect(result).toContain("<:mage:1558225227039637624> 50"); }); @@ -402,7 +402,7 @@ describe.concurrent("embedGenerator", () => { }; const result = getLongDescription(embedData); - expect(result).toContain("<:slayyyter:1558225235713593354> **Slayer: 5M**"); + expect(result).toContain("<:slayyyter:1558225235713593354> **Slayer XP: 5M**"); expect(result).toContain("<:zombie:1558225246945939527> 9"); expect(result).toContain("<:spider:1558225239299727380> 8"); expect(result).toContain("<:wolf:1558225245532463225> 8"); @@ -442,7 +442,7 @@ describe.concurrent("embedGenerator", () => { }; const result = getLongDescription(embedData); - expect(result).not.toContain("<:slayyyter:1558225235713593354> **Slayer:"); + expect(result).not.toContain("<:slayyyter:1558225235713593354> **Slayer XP:"); }); it("should not include slayers section when xp is undefined", ({ expect }) => { @@ -457,7 +457,7 @@ describe.concurrent("embedGenerator", () => { }; const result = getLongDescription(embedData); - expect(result).not.toContain("<:slayyyter:1558225235713593354> **Slayer:"); + expect(result).not.toContain("<:slayyyter:1558225235713593354> **Slayer XP:"); }); it("should skip slayers with undefined values", ({ expect }) => { @@ -517,11 +517,11 @@ describe.concurrent("embedGenerator", () => { expect(result).toContain("<:networth:1558225229418074112> Networth: 2B"); expect(result).toContain("<:purse:1558225230613450814> Purse: 75M"); expect(result).toContain("<:bank:1558225205909000202> Bank: 150M"); - expect(result).toContain("<:skills:1558225234547580948> **Skills: 50**"); + expect(result).toContain("<:skills:1558225234547580948> **Skill Average: 50**"); expect(result).toContain("<:farming:1558225218567147651> 60"); - expect(result).toContain("<:dungeons:1558225214444150794> **Dungeons: 45**"); + expect(result).toContain("<:dungeons:1558225214444150794> **Class Average: 45**"); expect(result).toContain("<:dungeoneering:1558225213223866399> 50"); - expect(result).toContain("<:slayyyter:1558225235713593354> **Slayer: 10M**"); + expect(result).toContain("<:slayyyter:1558225235713593354> **Slayer XP: 10M**"); expect(result).toContain("<:zombie:1558225246945939527> 9"); }); diff --git a/src/lib/shared/embedGenerator.ts b/src/lib/shared/embedGenerator.ts index d5259cd5f..fea90ef76 100644 --- a/src/lib/shared/embedGenerator.ts +++ b/src/lib/shared/embedGenerator.ts @@ -79,7 +79,7 @@ function getLongDescription(embedData: ModelsEmbedData): string { // Skills const skills = embedData.skills?.skills; if (embedData.skills && skills && Object.keys(skills).length > 0) { - lines.push(`\n${miscEmojis.skills} **Skills: ${embedData.skills.skillAverage}**`); + lines.push(`\n${miscEmojis.skills} **Skill Average: ${embedData.skills.skillAverage}**`); lines.push("-# ~~ ~~"); const skillsOrder: SkillKey[][] = [ @@ -103,7 +103,7 @@ function getLongDescription(embedData: ModelsEmbedData): string { if (embedData.dungeons && embedData.dungeons.classAverage && embedData.dungeons.classAverage) { const classAverage = embedData.dungeons.classAverage; if (classAverage) { - lines.push(`\n${skillEmojis.dungeons} **Dungeons: ${classAverage}**`); + lines.push(`\n${skillEmojis.dungeons} **Class Average: ${classAverage}**`); lines.push("-# ~~ ~~"); } @@ -126,7 +126,7 @@ function getLongDescription(embedData: ModelsEmbedData): string { // Slayer if (embedData.slayers && embedData.slayers.xp != null && embedData.slayers.xp > 0) { - lines.push(`\n${miscEmojis.slayyyter} **Slayer: ${formatNumber(embedData.slayers.xp)}**`); + lines.push(`\n${miscEmojis.slayyyter} **Slayer XP: ${formatNumber(embedData.slayers.xp)}**`); lines.push("-# ~~ ~~"); if (embedData.slayers.slayers) { From 23a523532e0b07751c46bf42ec8f0a27a629a0be Mon Sep 17 00:00:00 2001 From: DarthGigi <47110839+DarthGigi@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:05:20 +0200 Subject: [PATCH 026/135] feat: implement generic title and description functions for SEO --- src/lib/components/misc/SEO.svelte | 35 +++++----- src/lib/shared/embedGenerator.spec.ts | 98 ++++++++++++++++++++++++++- src/lib/shared/embedGenerator.ts | 58 +++++++++++++++- 3 files changed, 173 insertions(+), 18 deletions(-) diff --git a/src/lib/components/misc/SEO.svelte b/src/lib/components/misc/SEO.svelte index 328f8ee34..c198aff4e 100644 --- a/src/lib/components/misc/SEO.svelte +++ b/src/lib/components/misc/SEO.svelte @@ -2,12 +2,11 @@ import { page } from "$app/state"; import { JsonLd } from "$lib/components/misc"; import type { ModelsEmbedData } from "$lib/shared/api/orval-generated"; - import { getLongDescription, getMetaTitle, getShortDescription } from "$lib/shared/embedGenerator"; + import { getGenericDescription, getGenericTitle } from "$lib/shared/embedGenerator"; import { mode } from "mode-watcher"; import SvelteSeo from "svelte-seo"; const { embedData }: { embedData: ModelsEmbedData } = $props(); - const isValidEmbed = $derived(!!embedData.username); const isStatsPage = $derived(page.url.pathname.includes("/stats/")); const routeIgn = $derived(page.params.ign); const routeProfile = $derived(page.params.profile || embedData.profile_cute_name); @@ -18,9 +17,13 @@ : `/stats/${encodeURIComponent(profileIdentifier)}` ); const canonicalUrl = $derived(`https://sky.shiiyu.moe${canonicalPath}`); - const profileDescription = $derived( - isStatsPage && !isValidEmbed ? getShortDescription(embedData) : getLongDescription(embedData) + const seoTitle = $derived( + embedData.username || embedData.displayName + ? `${getGenericTitle(embedData)} | SkyCrypt` + : `${profileIdentifier} | SkyCrypt` ); + const seoDescription = $derived(getGenericDescription(embedData)); + const ogTitle = $derived(getGenericTitle(embedData)); const profileImage = $derived(`https://nmsr.nickac.dev/bust/${embedData.uuid}?y=-20`); const themeColor = $derived( embedData.rank?.plusColor || embedData.rank?.rankColor || (mode.current === "light" ? "#dbdbdb" : "#282828") @@ -53,14 +56,14 @@ "@context": "https://schema.org", "@type": "ProfilePage", "@id": `${canonicalUrl}#profilepage`, - name: getMetaTitle(embedData) || profileIdentifier, - description: profileDescription, + name: seoTitle, + description: seoDescription, url: canonicalUrl, mainEntity: { "@type": "Person", "@id": `${canonicalUrl}#person`, name: embedData.username || profileIdentifier, - alternateName: embedData.username || profileIdentifier, + alternateName: embedData.displayName || embedData.username || profileIdentifier, image: profileImage, url: canonicalUrl }, @@ -81,12 +84,12 @@ diff --git a/src/lib/shared/embedGenerator.spec.ts b/src/lib/shared/embedGenerator.spec.ts index e6ca692b5..bbbefc586 100644 --- a/src/lib/shared/embedGenerator.spec.ts +++ b/src/lib/shared/embedGenerator.spec.ts @@ -1,5 +1,11 @@ import type { ModelsEmbedData } from "$lib/shared/api/orval-generated"; -import { getLongDescription, getMetaTitle, getShortDescription } from "$lib/shared/embedGenerator"; +import { + getGenericDescription, + getGenericTitle, + getLongDescription, + getMetaTitle, + getShortDescription +} from "$lib/shared/embedGenerator"; import { afterEach, describe, it, vi } from "vitest"; describe.concurrent("embedGenerator", () => { @@ -671,4 +677,94 @@ describe.concurrent("embedGenerator", () => { expect(result).toBe("Player_123 has been playing SkyBlock for 1 year"); }); }); + + describe("getGenericTitle", () => { + it("returns default title when embedData is empty or missing username/displayName", ({ expect }) => { + // @ts-expect-error null case + expect(getGenericTitle(null)).toBe("SkyBlock Stats | SkyCrypt"); + expect(getGenericTitle({})).toBe("SkyBlock Stats | SkyCrypt"); + }); + + it("returns formatted title with player and profile name", ({ expect }) => { + const embedData: ModelsEmbedData = { + username: "tiltedhoney", + displayName: "TiltedHoney", + profile_cute_name: "Kiwi" + }; + expect(getGenericTitle(embedData)).toBe("TiltedHoney (Kiwi) | SkyBlock Stats"); + }); + + it("includes gamemode when not normal", ({ expect }) => { + const embedData: ModelsEmbedData = { + username: "ironman_steve", + displayName: "Steve", + profile_cute_name: "Apple", + game_mode: "ironman" + }; + expect(getGenericTitle(embedData)).toBe("Steve (Apple [ironman]) | SkyBlock Stats"); + }); + + it("returns title without profile when profile is missing", ({ expect }) => { + const embedData: ModelsEmbedData = { + username: "tiltedhoney", + displayName: "TiltedHoney" + }; + expect(getGenericTitle(embedData)).toBe("TiltedHoney | SkyBlock Stats"); + }); + }); + + describe("getGenericDescription", () => { + it("returns default description when embedData is empty", ({ expect }) => { + // @ts-expect-error null case + expect(getGenericDescription(null)).toBe("A beautiful site for sharing your SkyBlock profile 🍣"); + expect(getGenericDescription({})).toBe("A beautiful site for sharing your SkyBlock profile 🍣"); + }); + + it("creates a clean, human-readable SEO summary with stats", ({ expect }) => { + const embedData: ModelsEmbedData = { + username: "tiltedhoney", + displayName: "TiltedHoney", + profile_cute_name: "Kiwi", + skyblock_level: 420.5, + networth: { normal: 15200000000 }, + skills: { skillAverage: 55.4 }, + dungeons: { classAverage: 50 }, + slayers: { xp: 12000000 } + }; + + const desc = getGenericDescription(embedData); + expect(desc).toBe( + "Check out TiltedHoney's SkyBlock profile (Kiwi) on SkyCrypt: Level 420.50 • Networth: 15.20B • Skill Avg: 55.4 • Class Avg: 50 • Slayer XP: 12M." + ); + // Ensure no Discord custom emoji IDs or Discord markdown tags are leaked + expect(desc).not.toContain("<:"); + expect(desc).not.toContain("-#"); + expect(desc).not.toContain("**"); + }); + + it("falls back to join date if no primary stats are present", ({ expect }) => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2025-06-15T00:00:00.000Z")); + + const embedData: ModelsEmbedData = { + username: "tiltedhoney", + displayName: "TiltedHoney", + joined: new Date("2023-01-01T00:00:00.000Z").getTime() + }; + + expect(getGenericDescription(embedData)).toBe("TiltedHoney has been playing SkyBlock for 2 years on SkyCrypt."); + vi.useRealTimers(); + }); + + it("falls back to generic profile overview if joined and stats are absent", ({ expect }) => { + const embedData: ModelsEmbedData = { + username: "tiltedhoney", + displayName: "TiltedHoney" + }; + + expect(getGenericDescription(embedData)).toBe( + "View TiltedHoney's Hypixel SkyBlock stats, skills, networth, dungeons, and gear on SkyCrypt." + ); + }); + }); }); diff --git a/src/lib/shared/embedGenerator.ts b/src/lib/shared/embedGenerator.ts index fea90ef76..6334a9275 100644 --- a/src/lib/shared/embedGenerator.ts +++ b/src/lib/shared/embedGenerator.ts @@ -166,4 +166,60 @@ function getShortDescription(embedData: ModelsEmbedData): string { return `${embedData.displayName} has been playing SkyBlock for ${formatDistanceToNowStrict(new Date(embedData.joined))}`; } -export { getLongDescription, getMetaTitle, getShortDescription }; +function getGenericDescription(embedData: ModelsEmbedData): string { + if (!embedData || (!embedData.username && !embedData.displayName)) { + return "A beautiful site for sharing your SkyBlock profile 🍣"; + } + + const name = embedData.displayName || embedData.username || "Player"; + const profileName = embedData.profile_cute_name ? ` (${embedData.profile_cute_name})` : ""; + const parts: string[] = []; + + if (embedData.skyblock_level != null) { + parts.push(`Level ${formatNumber(embedData.skyblock_level)}`); + } + + if (embedData.networth?.normal != null) { + parts.push(`Networth: ${formatNumber(embedData.networth.normal)}`); + } + + if (embedData.skills?.skillAverage != null) { + parts.push(`Skill Avg: ${embedData.skills.skillAverage}`); + } + + if (embedData.dungeons?.classAverage != null) { + parts.push(`Class Avg: ${embedData.dungeons.classAverage}`); + } + + if (embedData.slayers?.xp != null && embedData.slayers.xp > 0) { + parts.push(`Slayer XP: ${formatNumber(embedData.slayers.xp)}`); + } + + if (parts.length > 0) { + return `Check out ${name}'s SkyBlock profile${profileName} on SkyCrypt: ${parts.join(" • ")}.`; + } + + if (embedData.joined) { + return `${name} has been playing SkyBlock for ${formatDistanceToNowStrict(new Date(embedData.joined))} on SkyCrypt.`; + } + + return `View ${name}'s Hypixel SkyBlock stats, skills, networth, dungeons, and gear on SkyCrypt.`; +} + +function getGenericTitle(embedData: ModelsEmbedData): string { + if (!embedData || (!embedData.username && !embedData.displayName)) { + return "SkyBlock Stats | SkyCrypt"; + } + + const name = embedData.displayName || embedData.username; + const profile = embedData.profile_cute_name; + const gameMode = embedData.game_mode && embedData.game_mode !== "normal" ? ` [${embedData.game_mode}]` : ""; + + if (profile) { + return `${name} (${profile}${gameMode}) | SkyBlock Stats`; + } + + return `${name} | SkyBlock Stats`; +} + +export { getGenericDescription, getGenericTitle, getLongDescription, getMetaTitle, getShortDescription }; From 2b3318c5ebac2126bbff476d67544bdda360d606 Mon Sep 17 00:00:00 2001 From: DarthGigi <47110839+DarthGigi@users.noreply.github.com> Date: Sat, 10 Oct 2026 16:22:09 +0200 Subject: [PATCH 027/135] feat: enhance SEO with structured data and canonical URLs across multiple pages --- src/lib/components/misc/SEO.svelte | 1 + src/routes/(protected)/dashboard/+page.svelte | 6 ++ src/routes/+layout.svelte | 38 ++++++++++-- src/routes/login/+page.svelte | 3 + src/routes/newsroom/+page.svelte | 23 +++++++ src/routes/newsroom/[slug]/+page.svelte | 61 +++++++++++++++++++ 6 files changed, 127 insertions(+), 5 deletions(-) diff --git a/src/lib/components/misc/SEO.svelte b/src/lib/components/misc/SEO.svelte index c198aff4e..e6432e440 100644 --- a/src/lib/components/misc/SEO.svelte +++ b/src/lib/components/misc/SEO.svelte @@ -90,6 +90,7 @@ openGraph={{ title: ogTitle, description: seoDescription, + url: canonicalUrl, type: "profile", profile: { username: embedData.username diff --git a/src/routes/(protected)/dashboard/+page.svelte b/src/routes/(protected)/dashboard/+page.svelte index 08419237d..d7135092e 100644 --- a/src/routes/(protected)/dashboard/+page.svelte +++ b/src/routes/(protected)/dashboard/+page.svelte @@ -1 +1,7 @@ + + + + Nothing here yet diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index a5c373ff9..cd8c65b13 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -61,7 +61,7 @@ const internalState = initInternalState(); const position = writable("bottom-right"); const theme = writable("dark"); - const noEmbedUrls = ["/stats/", "/newsroom"]; + const noEmbedUrls = ["/stats/", "/newsroom", "/login", "/dashboard"]; const packs = new PacksContext(); const websiteJsonLd = { "@context": "https://schema.org", @@ -75,7 +75,15 @@ "@id": "https://sky.shiiyu.moe/#organization", name: "SkyCrypt", url: "https://sky.shiiyu.moe", - logo: "https://sky.shiiyu.moe/img/app-icons/svg.svg" + logo: "https://sky.shiiyu.moe/img/app-icons/512.png" + }, + potentialAction: { + "@type": "SearchAction", + target: { + "@type": "EntryPoint", + urlTemplate: "https://sky.shiiyu.moe/stats/{search_term_string}" + }, + "query-input": "required name=search_term_string" } } as const; @@ -277,13 +285,33 @@ diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte index 012b03f38..859f5f7e1 100644 --- a/src/routes/login/+page.svelte +++ b/src/routes/login/+page.svelte @@ -1,9 +1,12 @@ + +

import { afterNavigate, goto } from "$app/navigation"; import { getNewsroomNotifications } from "$ctx"; + import { JsonLd } from "$lib/components/misc"; import Pagination from "$lib/components/newsroom/Pagination.svelte"; import PostCard from "$lib/components/newsroom/PostCard.svelte"; import { POST_TYPE_LABELS } from "$lib/components/newsroom/TypeBadge.svelte"; @@ -27,6 +28,25 @@ const markVisiblePostsSeen = () => notifications.markAllSeen(data.docs); afterNavigate(markVisiblePostsSeen); + + const breadcrumbJsonLd = { + "@context": "https://schema.org", + "@type": "BreadcrumbList", + itemListElement: [ + { + "@type": "ListItem", + position: 1, + name: "Home", + item: "https://sky.shiiyu.moe" + }, + { + "@type": "ListItem", + position: 2, + name: "Newsroom", + item: "https://sky.shiiyu.moe/newsroom" + } + ] + } as const; + +
+ + +
diff --git a/src/lib/components/ui/emoji-picker/emoji-picker-viewport.svelte b/src/lib/components/ui/emoji-picker/emoji-picker-viewport.svelte new file mode 100644 index 000000000..e2ef0e94e --- /dev/null +++ b/src/lib/components/ui/emoji-picker/emoji-picker-viewport.svelte @@ -0,0 +1,10 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/emoji-picker/emoji-picker.svelte b/src/lib/components/ui/emoji-picker/emoji-picker.svelte new file mode 100644 index 000000000..7b6bd1cb1 --- /dev/null +++ b/src/lib/components/ui/emoji-picker/emoji-picker.svelte @@ -0,0 +1,49 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/emoji-picker/emoji-picker.svelte.spec.ts b/src/lib/components/ui/emoji-picker/emoji-picker.svelte.spec.ts new file mode 100644 index 000000000..fe47ed4fb --- /dev/null +++ b/src/lib/components/ui/emoji-picker/emoji-picker.svelte.spec.ts @@ -0,0 +1,105 @@ +import EmojiPickerTest from "$src/test-utils/EmojiPickerTest.svelte"; +import data, { type EmojiMartData } from "@emoji-mart/data"; +import { beforeEach, expect, it, vi } from "vitest"; +import { render } from "vitest-browser-svelte"; +import { page, userEvent } from "vitest/browser"; + +const emojiData = data as EmojiMartData; +const names = emojiData.categories.flatMap((category) => category.emojis); +const optionNames = () => + Array.from(document.querySelectorAll('[role="option"]'), (item) => item.getAttribute("data-value")); + +beforeEach(async () => { + localStorage.removeItem("emoji-picker-performance-test"); + await page.viewport(1024, 768); +}); + +it("mounts one page and makes every category reachable by scrolling", async () => { + await render(EmojiPickerTest); + await expect.poll(() => optionNames().length).toBe(120); + await page.getByRole("combobox", { name: "Search emojis" }).click(); + const list = document.querySelector('[data-slot="command-list"]')!; + for (let count = 120; count < names.length; count += 120) { + list.scrollTop = list.scrollHeight; + const scrollTop = list.scrollTop; + await expect.poll(() => optionNames().length).toBe(Math.min(count + 120, names.length)); + await expect.poll(() => list.scrollTop).toBe(scrollTop); + } + expect(optionNames()).toEqual(names); +}); + +it("preserves trimmed, case-insensitive substring search and resets the render budget", async () => { + await render(EmojiPickerTest); + const list = document.querySelector('[data-slot="command-list"]')!; + list.scrollTop = list.scrollHeight; + await expect.poll(() => optionNames().length).toBe(240); + const search = page.getByRole("combobox", { name: "Search emojis" }); + await search.fill(" UCK "); + const expected = names.filter((name) => { + const emoji = emojiData.emojis[name]; + return [emoji.id, emoji.name, ...emoji.keywords].some((keyword) => keyword.toLowerCase().includes("uck")); + }); + await expect.poll(optionNames).toEqual(expected); + await search.fill("no-emoji-matches-this-query"); + await expect.element(page.getByText("No results.")).toBeVisible(); + await search.fill(""); + await expect.poll(() => optionNames().length).toBe(120); +}); + +it("reacts to allowed emoji changes and restricts search and recents", async () => { + localStorage.setItem( + "emoji-picker-performance-test", + JSON.stringify({ "duck:0": { uses: 1, lastUsage: 1 }, "purple_heart:0": { uses: 2, lastUsage: 2 } }) + ); + const view = await render(EmojiPickerTest, { showRecents: true, allowedEmojis: ["💜"] }); + await expect.poll(optionNames).toEqual(["purple_heart:0:recent", "purple_heart"]); + const search = page.getByRole("combobox", { name: "Search emojis" }); + await search.fill("duck"); + await expect.element(page.getByText("No results.")).toBeVisible(); + await view.rerender({ allowedEmojis: ["🦆"] }); + await expect.poll(optionNames).toEqual(["duck:0:recent", "duck"]); + await search.fill(""); + await view.rerender({ allowedEmojis: [] }); + await expect.poll(optionNames).toEqual([]); +}); + +it("selects with the keyboard and preserves skin tones in recents", async () => { + const onSelect = vi.fn(); + await render(EmojiPickerTest, { showRecents: true, onSelect }); + await page.getByRole("button", { name: "Change skin tone" }).click(); + const search = page.getByRole("combobox", { name: "Search emojis" }); + await search.fill("wave"); + await expect.element(page.getByRole("option", { name: "Waving Hand", exact: true })).toHaveTextContent("👋🏻"); + await userEvent.keyboard("{Home}{Enter}"); + expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ emoji: "👋🏻", skin: 1 })); + await expect.poll(() => optionNames()[0]).toBe("wave:1:recent"); + await page.getByRole("option", { name: "Waving Hand", exact: true }).first().click(); + expect(onSelect).toHaveBeenCalledTimes(2); + expect(onSelect.mock.calls[1][0].skin).toBe(1); +}); + +it("loads the next page when keyboard navigation reaches the rendered boundary", async () => { + const onSelect = vi.fn(); + await render(EmojiPickerTest, { onSelect }); + await page.getByRole("combobox", { name: "Search emojis" }).click(); + await userEvent.keyboard("{End}"); + await expect.poll(() => optionNames().length).toBe(240); + await userEvent.keyboard("{ArrowDown}{Enter}"); + expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ data: emojiData.emojis[names[125]] })); +}); + +it("preselects an emoji and fills the footer each time the picker opens", async () => { + const onSelect = vi.fn(); + await render(EmojiPickerTest, { inPopover: true, allowedEmojis: ["🦆", "💜"], onSelect }); + const trigger = page.getByRole("button", { name: "Open picker" }); + await trigger.click(); + await expect.element(page.getByLabelText("Emoji preview")).toHaveTextContent("💜 Purple Heart"); + expect(document.querySelector('[role="option"][data-selected]')?.getAttribute("data-value")).toBe("purple_heart"); + expect(onSelect).not.toHaveBeenCalled(); + await userEvent.keyboard("{Escape}"); + await expect.element(page.getByRole("combobox", { name: "Search emojis" })).not.toBeInTheDocument(); + await trigger.click(); + await expect.element(page.getByLabelText("Emoji preview")).toHaveTextContent("💜 Purple Heart"); + expect(document.querySelector('[role="option"][data-selected]')?.getAttribute("data-value")).toBe("purple_heart"); + expect(onSelect).not.toHaveBeenCalled(); +}); diff --git a/src/lib/components/ui/emoji-picker/emoji-picker.svelte.ts b/src/lib/components/ui/emoji-picker/emoji-picker.svelte.ts new file mode 100644 index 000000000..36febd8fe --- /dev/null +++ b/src/lib/components/ui/emoji-picker/emoji-picker.svelte.ts @@ -0,0 +1,301 @@ +import { UseFrecency } from "$hooks/use-frecency.svelte"; +import data, { type EmojiMartData } from "@emoji-mart/data"; +import { Context, watch } from "runed"; +import { tick } from "svelte"; +import type { ReadableBoxedValues, WritableBoxedValues } from "svelte-toolbelt"; +import type { EmojiPickerSkin, SelectedEmoji } from "./types"; + +const emojiData = data as EmojiMartData; +const RENDER_PAGE_SIZE = 120; +const emojiKeywords = Object.fromEntries( + Object.entries(emojiData.emojis).map(([name, emoji]) => [ + name, + [emoji.id, emoji.name, ...emoji.keywords].map((keyword) => keyword.toLowerCase()) + ]) +); + +type EmojiPickerState = { + search: string; + active: SelectedEmoji | null; +}; + +const defaultState: EmojiPickerState = { + search: "", + active: null +}; + +type EmojiPickerRootProps = WritableBoxedValues<{ + value: string; + skin: EmojiPickerSkin; +}> & + ReadableBoxedValues<{ + onSelect: (emoji: SelectedEmoji) => void; + showRecents: boolean; + recentsKey: string; + maxRecents: number; + onSkinChange: (skin: EmojiPickerSkin) => void; + }>; + +class EmojiPickerRootState { + emojiPickerState = $state({ ...defaultState }); + frecency: UseFrecency | null; + command = $state.raw<{ getValidItems: () => HTMLElement[]; updateSelectedToIndex: (index: number) => void }>(); + + constructor(readonly opts: EmojiPickerRootProps) { + this.select = this.select.bind(this); + this.onValueChange = this.onValueChange.bind(this); + + if (this.opts.showRecents.current) { + if (!this.opts.recentsKey.current) throw new Error("[emoji-picker] recentsKey is required when recents is true"); + + this.frecency = new UseFrecency(this.opts.recentsKey.current, {}, { maxItems: this.opts.maxRecents.current }); + } else { + this.frecency = null; + } + } + + select(emoji: string) { + const { name, skin } = parseValue(emoji); + + const selected = { + emoji: emojiData.emojis[name].skins[skin].native, + data: emojiData.emojis[name], + skin + }; + + this.opts.value.current = selected.emoji; + + this.opts.onSelect.current(selected); + } + + onValueChange(value: string) { + if (value === "") { + this.emojiPickerState.active = null; + return; + } + + const { name, skin } = parseValue(value); + + const emojiSkin = skin ? skin : this.opts.skin.current; + + const data = emojiData.emojis[name]; + + if (data.skins.length === 1) { + this.emojiPickerState.active = { + emoji: data.skins[0].native, + data: data, + skin: 0 + }; + return; + } + + this.emojiPickerState.active = { + emoji: data.skins[emojiSkin].native, + data: data, + skin: emojiSkin + }; + } +} + +export function parseValue(emojiKey: string): { name: string; skin: number } { + const [name, skin] = emojiKey.split(":"); + return { name, skin: skin ? Number(skin) : 0 }; +} + +export function makeValue(name: string, skin: number) { + return `${name}:${skin}`; +} + +type EmojiPickerListProps = ReadableBoxedValues<{ + allowedEmojis: string[] | undefined; +}>; + +class EmojiPickerListState { + renderLimit = $state(RENDER_PAGE_SIZE); + search = $derived.by(() => this.root.emojiPickerState.search.trim().toLowerCase()); + allowedEmojis = $derived.by(() => + this.opts.allowedEmojis.current ? new Set(this.opts.allowedEmojis.current) : null + ); + + filteredCategories = $derived.by(() => { + const search = this.search; + const allowedEmojis = this.allowedEmojis; + return emojiData.categories + .map((category) => ({ + id: category.id, + emojis: category.emojis.filter((name) => this.matches(name, search, allowedEmojis)) + })) + .filter((category) => category.emojis.length > 0); + }); + + recents = $derived.by(() => { + if (!this.showRecents) return []; + const search = this.search; + const allowedEmojis = this.allowedEmojis; + return (this.root.frecency?.items ?? []) + .filter((item) => this.matches(parseValue(item).name, search, allowedEmojis)) + .slice(0, this.maxRecents); + }); + + visibleGroups = $derived.by(() => { + let budget = this.renderLimit; + let remaining = 0; + const groups: { id: string; emojis: string[] }[] = []; + for (const category of this.filteredCategories) { + const emojis = category.emojis.slice(0, budget); + budget -= emojis.length; + remaining += category.emojis.length - emojis.length; + if (emojis.length > 0) groups.push({ id: category.id, emojis }); + } + return { groups, remaining }; + }); + + constructor( + readonly root: EmojiPickerRootState, + readonly opts: EmojiPickerListProps + ) { + this.select = this.select.bind(this); + watch([() => this.search, () => this.allowedEmojis], () => { + this.renderLimit = RENDER_PAGE_SIZE; + }); + } + + matches(name: string, search: string, allowedEmojis: Set | null) { + const emoji = emojiData.emojis[name]; + if (!emoji || (allowedEmojis && !allowedEmojis.has(emoji.skins[0].native))) return false; + return emojiKeywords[name].some((keyword) => keyword.includes(search)); + } + + async showMore(list: HTMLElement) { + const scrollTop = list.scrollTop; + const command = this.root.command; + const selectedIndex = command?.getValidItems().findIndex((item) => item.hasAttribute("data-selected")) ?? -1; + const categories = this.filteredCategories; + this.renderLimit += RENDER_PAGE_SIZE; + // Let Bits UI finish registering and selecting items before restoring the highlight. + await new Promise((resolve) => requestAnimationFrame(() => resolve())); + if (!list.isConnected || command !== this.root.command || categories !== this.filteredCategories) return; + if (selectedIndex >= 0) command?.updateSelectedToIndex(selectedIndex); + await tick(); + list.scrollTop = scrollTop; + } + + get skinIndex() { + return this.root.opts.skin.current; + } + + select(emoji: string) { + this.root.select(emoji); + } + + get maxRecents() { + if (this.root.opts.showRecents) { + return this.root.opts.maxRecents.current; + } + + return 0; + } + + get showRecents() { + return this.root.opts.showRecents.current && this.root.frecency !== null; + } +} + +type EmojiPickerInputProps = WritableBoxedValues<{ + value: string; +}>; + +class EmojiPickerInputState { + constructor( + readonly root: EmojiPickerRootState, + readonly opts: EmojiPickerInputProps + ) { + watch( + () => this.opts.value.current, + () => { + this.root.emojiPickerState.search = this.opts.value.current; + } + ); + } +} + +class EmojiPickerFooterState { + constructor(readonly root: EmojiPickerRootState) {} +} + +type EmojiPickerSkinProps = ReadableBoxedValues<{ + previewEmoji: string; +}>; + +class EmojiPickerSkinToneSelectorState { + constructor( + readonly root: EmojiPickerRootState, + readonly opts: EmojiPickerSkinProps + ) { + this.cycleSkinTone = this.cycleSkinTone.bind(this); + } + + previewEmoji = $derived.by(() => { + for (const emoji of Object.entries(emojiData.emojis)) { + const [_, data] = emoji; + + let found = false; + for (const skin of data.skins) { + if (skin.native === this.opts.previewEmoji.current) { + found = true; + break; + } + } + + if (!found) continue; + + if (data.skins.length === 0) { + throw new Error(`The selected previewEmoji: ${this.opts.previewEmoji.current} does not have multiple skins!`); + } + + return data; + } + + return null; + }); + + get preview() { + if (!this.previewEmoji) return null; + + return this.previewEmoji.skins[this.root.opts.skin.current].native; + } + + cycleSkinTone() { + if (!this.previewEmoji) return; + + if (this.root.opts.skin.current + 1 > 5) { + this.root.opts.skin.current = 0; + } else { + this.root.opts.skin.current += 1; + } + + this.root.opts.onSkinChange.current(this.root.opts.skin.current as EmojiPickerSkin); + } +} + +const ctx = new Context("emoji-picker-root-state"); + +export function useEmojiPicker(props: EmojiPickerRootProps) { + return ctx.set(new EmojiPickerRootState(props)); +} + +export function useEmojiPickerList(props: EmojiPickerListProps) { + return new EmojiPickerListState(ctx.get(), props); +} + +export function useEmojiPickerInput(props: EmojiPickerInputProps) { + return new EmojiPickerInputState(ctx.get(), props); +} + +export function useEmojiPickerFooter() { + return new EmojiPickerFooterState(ctx.get()); +} + +export function useEmojiPickerSkinToneSelector(props: EmojiPickerSkinProps) { + return new EmojiPickerSkinToneSelectorState(ctx.get(), props); +} diff --git a/src/lib/components/ui/emoji-picker/index.ts b/src/lib/components/ui/emoji-picker/index.ts new file mode 100644 index 000000000..d2f9540cd --- /dev/null +++ b/src/lib/components/ui/emoji-picker/index.ts @@ -0,0 +1,22 @@ +import Footer from "./emoji-picker-footer.svelte"; +import List from "./emoji-picker-list.svelte"; +import Search from "./emoji-picker-search.svelte"; +import SkinToneSelector from "./emoji-picker-skin-tone-selector.svelte"; +import Viewport from "./emoji-picker-viewport.svelte"; +import Root from "./emoji-picker.svelte"; + +export { Root, List, Viewport, Search, Footer, SkinToneSelector }; + +export type { + SelectedEmoji, + EmojiPickerSkin, + EmojiPickerRootPropsWithoutHTML, + EmojiPickerRootProps, + EmojiPickerListPropsWithoutHTML, + EmojiPickerListProps, + EmojiPickerSearchProps, + EmojiPickerFooterPropsWithoutHTML, + EmojiPickerFooterProps, + EmojiPickerSkinPropsWithoutHTML, + EmojiPickerSkinProps +} from "./types"; diff --git a/src/lib/components/ui/emoji-picker/types.ts b/src/lib/components/ui/emoji-picker/types.ts new file mode 100644 index 000000000..aaa8771fd --- /dev/null +++ b/src/lib/components/ui/emoji-picker/types.ts @@ -0,0 +1,96 @@ +import type { ButtonProps } from "$ui/button"; +import type { Emoji } from "@emoji-mart/data"; +import type { WithChildren, WithoutChild, WithoutChildren } from "bits-ui"; +import type { Command as CommandPrimitive } from "bits-ui"; +import type { Snippet } from "svelte"; +import type { HTMLAttributes, HTMLButtonAttributes } from "svelte/elements"; + +export type SelectedEmoji = { + emoji: string; + data: Emoji; + skin: number; +}; + +/** + * The skin tone modifier for the emoji + * + * 0 = 👋 + * 1 = 👋🏻 + * 2 = 👋🏼 + * 3 = 👋🏽 + * 4 = 👋🏾 + * 5 = 👋🏿 + */ +export type EmojiPickerSkin = 0 | 1 | 2 | 3 | 4 | 5; + +export type EmojiPickerRootPropsWithoutHTML = WithChildren<{ + /** + * The default skin to use + * + * @default 0 + * + * ``` + * 0 = 👋 + * 1 = 👋🏻 + * 2 = 👋🏼 + * 3 = 👋🏽 + * 4 = 👋🏾 + * 5 = 👋🏿 + * ``` + */ + skin?: EmojiPickerSkin; + onSelect?: (emoji: SelectedEmoji) => void; + onSkinChange?: (skin: EmojiPickerSkin) => void; +}> & + ( + | { + /** Show recently used emojis */ + showRecents?: true; + /** The key to use to store the recently used emojis */ + recentsKey: string; + maxRecents?: number; + } + | { + /** Show recently used emojis */ + showRecents?: false | never; + /** The key to use to store the recently used emojis */ + recentsKey?: never; + maxRecents?: never; + } + ); + +export type EmojiPickerRootProps = WithoutChild< + Omit +> & + EmojiPickerRootPropsWithoutHTML; + +export type EmojiPickerListPropsWithoutHTML = { + /** Restrict the list to these base emoji. */ + allowedEmojis?: string[]; + emptyMessage?: string; +}; + +export type EmojiPickerListProps = WithoutChildren> & + EmojiPickerListPropsWithoutHTML; + +export type EmojiPickerSearchProps = CommandPrimitive.InputProps; + +export type EmojiPickerFooterPropsWithoutHTML = { + children: Snippet<[{ active: SelectedEmoji | null }]>; +}; + +export type EmojiPickerFooterProps = WithoutChildren> & + EmojiPickerFooterPropsWithoutHTML; + +export type EmojiPickerSkinPropsWithoutHTML = { + /** + * The emoji to use to preview the skin tone + * + * @default "👋" + */ + previewEmoji?: string; +}; + +export type EmojiPickerSkinProps = EmojiPickerSkinPropsWithoutHTML & + WithoutChildren & + Pick; diff --git a/src/lib/components/ui/field/field-content.svelte b/src/lib/components/ui/field/field-content.svelte new file mode 100644 index 000000000..999a96416 --- /dev/null +++ b/src/lib/components/ui/field/field-content.svelte @@ -0,0 +1,19 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/field/field-description.svelte b/src/lib/components/ui/field/field-description.svelte new file mode 100644 index 000000000..34b7ecdb7 --- /dev/null +++ b/src/lib/components/ui/field/field-description.svelte @@ -0,0 +1,24 @@ + + +

a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary", + className + )} + {...restProps}> + {@render children?.()} +

diff --git a/src/lib/components/ui/field/field-error.svelte b/src/lib/components/ui/field/field-error.svelte new file mode 100644 index 000000000..96121bc05 --- /dev/null +++ b/src/lib/components/ui/field/field-error.svelte @@ -0,0 +1,57 @@ + + +{#if hasContent} + +{/if} diff --git a/src/lib/components/ui/field/field-group.svelte b/src/lib/components/ui/field/field-group.svelte new file mode 100644 index 000000000..623a70edc --- /dev/null +++ b/src/lib/components/ui/field/field-group.svelte @@ -0,0 +1,22 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/field/field-label.svelte b/src/lib/components/ui/field/field-label.svelte new file mode 100644 index 000000000..01d141e9e --- /dev/null +++ b/src/lib/components/ui/field/field-label.svelte @@ -0,0 +1,19 @@ + + + diff --git a/src/lib/components/ui/field/field-legend.svelte b/src/lib/components/ui/field/field-legend.svelte new file mode 100644 index 000000000..87ba0f5a3 --- /dev/null +++ b/src/lib/components/ui/field/field-legend.svelte @@ -0,0 +1,23 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/field/field-separator.svelte b/src/lib/components/ui/field/field-separator.svelte new file mode 100644 index 000000000..74a1c2791 --- /dev/null +++ b/src/lib/components/ui/field/field-separator.svelte @@ -0,0 +1,33 @@ + + +
+ + {#if children} + + {@render children()} + + {/if} +
diff --git a/src/lib/components/ui/field/field-set.svelte b/src/lib/components/ui/field/field-set.svelte new file mode 100644 index 000000000..bf6c2d81b --- /dev/null +++ b/src/lib/components/ui/field/field-set.svelte @@ -0,0 +1,22 @@ + + +
[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", + className + )} + {...restProps}> + {@render children?.()} +
diff --git a/src/lib/components/ui/field/field-title.svelte b/src/lib/components/ui/field/field-title.svelte new file mode 100644 index 000000000..6dd88dec0 --- /dev/null +++ b/src/lib/components/ui/field/field-title.svelte @@ -0,0 +1,22 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/field/field.svelte b/src/lib/components/ui/field/field.svelte new file mode 100644 index 000000000..a9a76044e --- /dev/null +++ b/src/lib/components/ui/field/field.svelte @@ -0,0 +1,46 @@ + + + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/field/index.ts b/src/lib/components/ui/field/index.ts new file mode 100644 index 000000000..b65338313 --- /dev/null +++ b/src/lib/components/ui/field/index.ts @@ -0,0 +1,33 @@ +import Content from "./field-content.svelte"; +import Description from "./field-description.svelte"; +import Error from "./field-error.svelte"; +import Group from "./field-group.svelte"; +import Label from "./field-label.svelte"; +import Legend from "./field-legend.svelte"; +import Separator from "./field-separator.svelte"; +import Set from "./field-set.svelte"; +import Title from "./field-title.svelte"; +import Field from "./field.svelte"; + +export { + Field, + Set, + Legend, + Group, + Content, + Label, + Title, + Description, + Separator, + Error, + // + Set as FieldSet, + Legend as FieldLegend, + Group as FieldGroup, + Content as FieldContent, + Label as FieldLabel, + Title as FieldTitle, + Description as FieldDescription, + Separator as FieldSeparator, + Error as FieldError +}; diff --git a/src/lib/components/ui/separator/separator.svelte b/src/lib/components/ui/separator/separator.svelte index 03bfcdfb4..e89a15c63 100644 --- a/src/lib/components/ui/separator/separator.svelte +++ b/src/lib/components/ui/separator/separator.svelte @@ -14,9 +14,7 @@ bind:ref data-slot={dataSlot} class={cn( - "shrink-0 bg-border data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-px", - // this is different in shadcn/ui but self-stretch breaks things for us - "data-[orientation=vertical]:h-full", + "shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch", className )} {...restProps} /> diff --git a/src/lib/components/ui/sheet/sheet-overlay.svelte b/src/lib/components/ui/sheet/sheet-overlay.svelte index c30fd361c..532a79b0a 100644 --- a/src/lib/components/ui/sheet/sheet-overlay.svelte +++ b/src/lib/components/ui/sheet/sheet-overlay.svelte @@ -8,5 +8,8 @@ diff --git a/src/lib/components/ui/sidebar/constants.ts b/src/lib/components/ui/sidebar/constants.ts new file mode 100644 index 000000000..1347e83c7 --- /dev/null +++ b/src/lib/components/ui/sidebar/constants.ts @@ -0,0 +1,6 @@ +export const SIDEBAR_COOKIE_NAME = "sidebar_state"; +export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; +export const SIDEBAR_WIDTH = "16rem"; +export const SIDEBAR_WIDTH_MOBILE = "18rem"; +export const SIDEBAR_WIDTH_ICON = "3rem"; +export const SIDEBAR_KEYBOARD_SHORTCUT = "b"; diff --git a/src/lib/components/ui/sidebar/context.svelte.ts b/src/lib/components/ui/sidebar/context.svelte.ts new file mode 100644 index 000000000..55e994b4e --- /dev/null +++ b/src/lib/components/ui/sidebar/context.svelte.ts @@ -0,0 +1,77 @@ +import { IsMobile } from "$hooks/is-mobile.svelte.js"; +import { getContext, setContext } from "svelte"; +import { SIDEBAR_KEYBOARD_SHORTCUT } from "./constants.js"; + +type Getter = () => T; + +export type SidebarStateProps = { + /** + * A getter function that returns the current open state of the sidebar. We use a getter function here to support + * `bind:open` on the `Sidebar.Provider` component. + */ + open: Getter; + + /** + * A function that sets the open state of the sidebar. To support `bind:open`, we need a source of truth for changing + * the open state to ensure it will be synced throughout the sub-components and any `bind:` references. + */ + setOpen: (open: boolean) => void; +}; + +class SidebarState { + readonly props: SidebarStateProps; + open = $derived.by(() => this.props.open()); + openMobile = $state(false); + setOpen: SidebarStateProps["setOpen"]; + #isMobile: IsMobile; + state = $derived.by(() => (this.open ? "expanded" : "collapsed")); + + constructor(props: SidebarStateProps) { + this.setOpen = props.setOpen; + this.#isMobile = new IsMobile(); + this.props = props; + } + + // Convenience getter for checking if the sidebar is mobile + // without this, we would need to use `sidebar.isMobile.current` everywhere + get isMobile() { + return this.#isMobile.current; + } + + // Event handler to apply to the `` + handleShortcutKeydown = (e: KeyboardEvent) => { + if (e.key === SIDEBAR_KEYBOARD_SHORTCUT && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + this.toggle(); + } + }; + + setOpenMobile = (value: boolean) => { + this.openMobile = value; + }; + + toggle = () => { + return this.#isMobile.current ? (this.openMobile = !this.openMobile) : this.setOpen(!this.open); + }; +} + +const SYMBOL_KEY = "scn-sidebar"; + +/** + * Instantiates a new `SidebarState` instance and sets it in the context. + * + * @param props The constructor props for the `SidebarState` class. + * @returns The `SidebarState` instance. + */ +export function setSidebar(props: SidebarStateProps): SidebarState { + return setContext(Symbol.for(SYMBOL_KEY), new SidebarState(props)); +} + +/** + * Retrieves the `SidebarState` instance from the context. This is a class instance, so you cannot destructure it. + * + * @returns The `SidebarState` instance. + */ +export function useSidebar(): SidebarState { + return getContext(Symbol.for(SYMBOL_KEY)); +} diff --git a/src/lib/components/ui/sidebar/index.ts b/src/lib/components/ui/sidebar/index.ts new file mode 100644 index 000000000..7bbe5f3d1 --- /dev/null +++ b/src/lib/components/ui/sidebar/index.ts @@ -0,0 +1,75 @@ +import { useSidebar } from "./context.svelte.js"; +import Content from "./sidebar-content.svelte"; +import Footer from "./sidebar-footer.svelte"; +import GroupAction from "./sidebar-group-action.svelte"; +import GroupContent from "./sidebar-group-content.svelte"; +import GroupLabel from "./sidebar-group-label.svelte"; +import Group from "./sidebar-group.svelte"; +import Header from "./sidebar-header.svelte"; +import Input from "./sidebar-input.svelte"; +import Inset from "./sidebar-inset.svelte"; +import MenuAction from "./sidebar-menu-action.svelte"; +import MenuBadge from "./sidebar-menu-badge.svelte"; +import MenuButton from "./sidebar-menu-button.svelte"; +import MenuItem from "./sidebar-menu-item.svelte"; +import MenuSkeleton from "./sidebar-menu-skeleton.svelte"; +import MenuSubButton from "./sidebar-menu-sub-button.svelte"; +import MenuSubItem from "./sidebar-menu-sub-item.svelte"; +import MenuSub from "./sidebar-menu-sub.svelte"; +import Menu from "./sidebar-menu.svelte"; +import Provider from "./sidebar-provider.svelte"; +import Rail from "./sidebar-rail.svelte"; +import Separator from "./sidebar-separator.svelte"; +import Trigger from "./sidebar-trigger.svelte"; +import Root from "./sidebar.svelte"; + +export { + Content, + Footer, + Group, + GroupAction, + GroupContent, + GroupLabel, + Header, + Input, + Inset, + Menu, + MenuAction, + MenuBadge, + MenuButton, + MenuItem, + MenuSkeleton, + MenuSub, + MenuSubButton, + MenuSubItem, + Provider, + Rail, + Root, + Separator, + // + Root as Sidebar, + Content as SidebarContent, + Footer as SidebarFooter, + Group as SidebarGroup, + GroupAction as SidebarGroupAction, + GroupContent as SidebarGroupContent, + GroupLabel as SidebarGroupLabel, + Header as SidebarHeader, + Input as SidebarInput, + Inset as SidebarInset, + Menu as SidebarMenu, + MenuAction as SidebarMenuAction, + MenuBadge as SidebarMenuBadge, + MenuButton as SidebarMenuButton, + MenuItem as SidebarMenuItem, + MenuSkeleton as SidebarMenuSkeleton, + MenuSub as SidebarMenuSub, + MenuSubButton as SidebarMenuSubButton, + MenuSubItem as SidebarMenuSubItem, + Provider as SidebarProvider, + Rail as SidebarRail, + Separator as SidebarSeparator, + Trigger as SidebarTrigger, + Trigger, + useSidebar +}; diff --git a/src/lib/components/ui/sidebar/sidebar-content.svelte b/src/lib/components/ui/sidebar/sidebar-content.svelte new file mode 100644 index 000000000..e1ddb07eb --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-content.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/sidebar/sidebar-footer.svelte b/src/lib/components/ui/sidebar/sidebar-footer.svelte new file mode 100644 index 000000000..fe560544d --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/sidebar/sidebar-group-action.svelte b/src/lib/components/ui/sidebar/sidebar-group-action.svelte new file mode 100644 index 000000000..c4fee11d5 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-group-action.svelte @@ -0,0 +1,33 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + +{/if} diff --git a/src/lib/components/ui/sidebar/sidebar-group-content.svelte b/src/lib/components/ui/sidebar/sidebar-group-content.svelte new file mode 100644 index 000000000..376b68c00 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-group-content.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/sidebar/sidebar-group-label.svelte b/src/lib/components/ui/sidebar/sidebar-group-label.svelte new file mode 100644 index 000000000..8b394041d --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-group-label.svelte @@ -0,0 +1,33 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} +
+ {@render children?.()} +
+{/if} diff --git a/src/lib/components/ui/sidebar/sidebar-group.svelte b/src/lib/components/ui/sidebar/sidebar-group.svelte new file mode 100644 index 000000000..21d4fb67b --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-group.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/sidebar/sidebar-header.svelte b/src/lib/components/ui/sidebar/sidebar-header.svelte new file mode 100644 index 000000000..dd0426ab7 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/sidebar/sidebar-input.svelte b/src/lib/components/ui/sidebar/sidebar-input.svelte new file mode 100644 index 000000000..e94b8e10c --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-input.svelte @@ -0,0 +1,20 @@ + + + diff --git a/src/lib/components/ui/sidebar/sidebar-inset.svelte b/src/lib/components/ui/sidebar/sidebar-inset.svelte new file mode 100644 index 000000000..6b50fd206 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-inset.svelte @@ -0,0 +1,22 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/sidebar/sidebar-menu-action.svelte b/src/lib/components/ui/sidebar/sidebar-menu-action.svelte new file mode 100644 index 000000000..c8b8682ea --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-action.svelte @@ -0,0 +1,37 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + +{/if} diff --git a/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte b/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte new file mode 100644 index 000000000..2658c6565 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/sidebar/sidebar-menu-button.svelte b/src/lib/components/ui/sidebar/sidebar-menu-button.svelte new file mode 100644 index 000000000..1fbb1d89a --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-button.svelte @@ -0,0 +1,100 @@ + + + + +{#snippet Button({ props }: { props?: Record })} + {@const mergedProps = mergeProps(buttonProps, props)} + {#if child} + {@render child({ props: mergedProps })} + {:else} + + {/if} +{/snippet} + +{#if !tooltipContent} + {@render Button({})} +{:else} + + + {#snippet child({ props })} + {@render Button({ props })} + {/snippet} + + + +{/if} diff --git a/src/lib/components/ui/sidebar/sidebar-menu-item.svelte b/src/lib/components/ui/sidebar/sidebar-menu-item.svelte new file mode 100644 index 000000000..10e9c41a5 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-item.svelte @@ -0,0 +1,20 @@ + + +
  • + {@render children?.()} +
  • diff --git a/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte b/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte new file mode 100644 index 000000000..bf2c3c6c9 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte @@ -0,0 +1,34 @@ + + +
    + {#if showIcon} + + {/if} + + {@render children?.()} +
    diff --git a/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte b/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte new file mode 100644 index 000000000..6bfa2c353 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte @@ -0,0 +1,39 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + + {@render children?.()} + +{/if} diff --git a/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte b/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte new file mode 100644 index 000000000..f802bd86e --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte @@ -0,0 +1,20 @@ + + +
  • + {@render children?.()} +
  • diff --git a/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte b/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte new file mode 100644 index 000000000..09b73aadc --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte @@ -0,0 +1,23 @@ + + +
      + {@render children?.()} +
    diff --git a/src/lib/components/ui/sidebar/sidebar-menu.svelte b/src/lib/components/ui/sidebar/sidebar-menu.svelte new file mode 100644 index 000000000..ec0714137 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-menu.svelte @@ -0,0 +1,20 @@ + + +
      + {@render children?.()} +
    diff --git a/src/lib/components/ui/sidebar/sidebar-provider.svelte b/src/lib/components/ui/sidebar/sidebar-provider.svelte new file mode 100644 index 000000000..0c546049a --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-provider.svelte @@ -0,0 +1,44 @@ + + + + + +
    + {@render children?.()} +
    +
    diff --git a/src/lib/components/ui/sidebar/sidebar-rail.svelte b/src/lib/components/ui/sidebar/sidebar-rail.svelte new file mode 100644 index 000000000..a6bd3561e --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-rail.svelte @@ -0,0 +1,35 @@ + + + diff --git a/src/lib/components/ui/sidebar/sidebar-separator.svelte b/src/lib/components/ui/sidebar/sidebar-separator.svelte new file mode 100644 index 000000000..862e258ce --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-separator.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/lib/components/ui/sidebar/sidebar-trigger.svelte b/src/lib/components/ui/sidebar/sidebar-trigger.svelte new file mode 100644 index 000000000..db5914427 --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar-trigger.svelte @@ -0,0 +1,35 @@ + + + diff --git a/src/lib/components/ui/sidebar/sidebar.svelte b/src/lib/components/ui/sidebar/sidebar.svelte new file mode 100644 index 000000000..77bac80fe --- /dev/null +++ b/src/lib/components/ui/sidebar/sidebar.svelte @@ -0,0 +1,92 @@ + + +{#if collapsible === "none"} +
    + {@render children?.()} +
    +{:else if sidebar.isMobile} + sidebar.openMobile, (v) => sidebar.setOpenMobile(v)} {...restProps}> + button]:hidden", className)} + style="--sidebar-width: {SIDEBAR_WIDTH_MOBILE};" + {side}> + + Sidebar + Displays the mobile sidebar. + +
    + {@render children?.()} +
    +
    +
    +{:else} + +{/if} diff --git a/src/lib/components/ui/skeleton/skeleton.svelte b/src/lib/components/ui/skeleton/skeleton.svelte index 914cb69d2..11bd2f6a2 100644 --- a/src/lib/components/ui/skeleton/skeleton.svelte +++ b/src/lib/components/ui/skeleton/skeleton.svelte @@ -9,9 +9,5 @@ }: WithoutChildren>> = $props(); -
    +
    diff --git a/src/lib/components/ui/spinner/spinner.svelte b/src/lib/components/ui/spinner/spinner.svelte index db5d96c8e..930b68a8f 100644 --- a/src/lib/components/ui/spinner/spinner.svelte +++ b/src/lib/components/ui/spinner/spinner.svelte @@ -9,7 +9,7 @@ // we add name, color, and stroke for compatibility with different icon libraries props name, color, - stroke, + stroke = "currentColor", "aria-label": ariaLabel = "Loading", ...restProps }: SVGAttributes = $props(); diff --git a/src/lib/components/ui/tooltip/tooltip-content.svelte b/src/lib/components/ui/tooltip/tooltip-content.svelte index 4a927437c..0a7e647dd 100644 --- a/src/lib/components/ui/tooltip/tooltip-content.svelte +++ b/src/lib/components/ui/tooltip/tooltip-content.svelte @@ -36,11 +36,7 @@ {#snippet child({ props })}
    From bdd7b177bb35180ec8361a566ff60ff7fc1bb872 Mon Sep 17 00:00:00 2001 From: DarthGigi <47110839+DarthGigi@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:06:06 +0200 Subject: [PATCH 029/135] chore: oxfmt --- src/lib/components/ScrollAreaItems.svelte | 4 ++-- src/lib/components/misc/SearchTabs.svelte | 2 +- src/lib/sections/stats/Gear.svelte | 4 ++-- src/lib/sections/stats/Inventory.svelte | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib/components/ScrollAreaItems.svelte b/src/lib/components/ScrollAreaItems.svelte index 78ba05f35..e0589da0b 100644 --- a/src/lib/components/ScrollAreaItems.svelte +++ b/src/lib/components/ScrollAreaItems.svelte @@ -75,10 +75,10 @@ class="relative flex gap-2 pt-4 group-data-[orientation=horizontal]/scroll:w-max group-data-[orientation=horizontal]/scroll:flex-nowrap group-data-[orientation=vertical]/scroll:flex-wrap sm:gap-4"> {@render children?.()}
    + class="pointer-events-none sticky -right-1 z-10 -ml-40 w-36 scroll-fade-x self-stretch bg-linear-to-l from-background/80 to-transparent blur-xs group-data-[orientation=vertical]/scroll:hidden">
    + class="pointer-events-none sticky -bottom-1 z-10 -mt-36 h-36 w-full scroll-fade-y bg-linear-to-t from-background/80 to-transparent blur-xs group-data-[orientation=horizontal]/scroll:hidden">
    diff --git a/src/lib/components/misc/SearchTabs.svelte b/src/lib/components/misc/SearchTabs.svelte index 5aaae8cfd..48eb5eff6 100644 --- a/src/lib/components/misc/SearchTabs.svelte +++ b/src/lib/components/misc/SearchTabs.svelte @@ -110,7 +110,7 @@ {@render tabsListItems()}
    + class="pointer-events-none sticky -bottom-1 z-10 -mt-36 h-36 w-full scroll-fade-y bg-linear-to-t from-background/80 to-transparent blur-xs">
    diff --git a/src/lib/sections/stats/Gear.svelte b/src/lib/sections/stats/Gear.svelte index bdd07f4c0..1843caf1f 100644 --- a/src/lib/sections/stats/Gear.svelte +++ b/src/lib/sections/stats/Gear.svelte @@ -113,7 +113,7 @@
    {/each}
    + class="pointer-events-none sticky -right-2 z-10 -ml-42 h-82 w-36 scroll-fade-x self-stretch bg-linear-to-l from-background/80 to-transparent blur-xs md:-ml-39">
    @@ -136,7 +136,7 @@ {/each}
    + class="pointer-events-none sticky -right-2 z-10 -ml-42 h-82 w-36 scroll-fade-x self-stretch bg-linear-to-l from-background/80 to-transparent blur-xs md:-ml-39">
    diff --git a/src/lib/sections/stats/Inventory.svelte b/src/lib/sections/stats/Inventory.svelte index 24d3e4103..b5ee1dcfd 100644 --- a/src/lib/sections/stats/Inventory.svelte +++ b/src/lib/sections/stats/Inventory.svelte @@ -100,7 +100,7 @@ {@render inventoryTabItems(inventories, selectedTabName)}
    + class="pointer-events-none sticky -bottom-1 z-10 -mt-36 h-36 w-full scroll-fade-y bg-linear-to-t from-background/80 to-transparent blur-xs">
    From 4468767b8434af2c7737b8d18d1883640db05d1b Mon Sep 17 00:00:00 2001 From: DarthGigi <47110839+DarthGigi@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:31:44 +0200 Subject: [PATCH 030/135] feat(profile): add persistent profile emoji customization --- .changeset/discord-profile-assets.md | 5 + .changeset/emoji-picker-performance.md | 7 + .changeset/emoji-server-checks.md | 5 + .changeset/friendly-dashboard-pages.md | 7 + .changeset/minecraft-account-sync.md | 5 + .changeset/permanent-profile-emojis.md | 5 + .changeset/private-account-settings.md | 5 + .changeset/sidebar-account-menu.md | 5 + .env.example | 8 +- drizzle/0001_fresh_sally_floyd.sql | 61 + drizzle/meta/0001_snapshot.json | 991 ++++ drizzle/meta/_journal.json | 7 + orval.config.ts | 11 + package.json | 66 +- pnpm-lock.yaml | 2760 ++++++------ src/app.d.ts | 13 +- .../components/dashboard/EmojiEditor.svelte | 96 + .../dashboard/EmojiEditor.svelte.spec.ts | 96 + src/lib/hooks/use-frecency.svelte.ts | 55 + src/lib/layouts/stats/PlayerProfile.svelte | 17 +- src/lib/server/account-data.db.spec.ts | 129 + src/lib/server/account-data.ts | 116 + src/lib/server/auth.ts | 140 +- src/lib/server/db/schema/auth.schema.ts | 8 +- src/lib/server/db/schema/discord.schema.ts | 20 + src/lib/server/db/schema/index.ts | 4 + src/lib/server/db/schema/minecraft.schema.ts | 16 + src/lib/server/db/schema/patreon.schema.ts | 38 + .../server/db/schema/profile-emoji.schema.ts | 14 + src/lib/server/discord-profile.spec.ts | 195 + src/lib/server/discord-profile.ts | 58 + src/lib/server/minecraft-accounts.spec.ts | 99 + src/lib/server/minecraft-accounts.ts | 104 + src/lib/server/patreon-cooldown.spec.ts | 19 + src/lib/server/patreon-cooldown.ts | 17 + src/lib/server/patreon-entitlements.ts | 38 + src/lib/server/patreon.db.spec.ts | 66 + src/lib/server/patreon.spec.ts | 50 + src/lib/server/patreon.ts | 315 ++ src/lib/server/profile-emoji.db.spec.ts | 96 + src/lib/server/profile-emoji.spec.ts | 85 + src/lib/server/profile-emoji.ts | 58 + src/lib/shared/api/patreon-generated.ts | 3988 +++++++++++++++++ src/lib/shared/auth-client.ts | 4 +- src/lib/shared/profile-emoji.remote.spec.ts | 33 + src/lib/shared/profile-emoji.remote.ts | 15 + src/lib/shared/profile-emoji.spec.ts | 26 + src/lib/shared/profile-emoji.ts | 18 + .../(protected)/dashboard/+layout.svelte | 92 + src/routes/(protected)/dashboard/+page.svelte | 172 +- .../dashboard/dashboard-sidebar.svelte | 142 + .../dashboard/dashboard.remote.spec.ts | 119 + .../(protected)/dashboard/dashboard.remote.ts | 61 + .../dashboard/dashboard.svelte.spec.ts | 70 + .../dashboard/donator-perks/+page.svelte | 277 ++ .../dashboard/donator-perks/perks.remote.ts | 22 + .../donator-perks/perks.svelte.spec.ts | 156 + .../dashboard/minecraft-accounts.svelte | 78 + .../minecraft-accounts.svelte.spec.ts | 41 + .../dashboard/minecraft.remote.spec.ts | 75 + .../(protected)/dashboard/minecraft.remote.ts | 39 + .../dashboard/settings/+page.svelte | 274 ++ .../dashboard/settings/session-device.spec.ts | 15 + .../dashboard/settings/session-device.ts | 28 + .../settings/settings.remote.spec.ts | 106 + .../dashboard/settings/settings.remote.ts | 55 + .../settings/settings.svelte.spec.ts | 156 + src/routes/login/+page.svelte | 97 +- src/routes/login/login.remote.ts | 25 - src/test-utils/EmojiPickerTest.svelte | 42 + vite.config.ts | 3 +- 71 files changed, 10691 insertions(+), 1448 deletions(-) create mode 100644 .changeset/discord-profile-assets.md create mode 100644 .changeset/emoji-picker-performance.md create mode 100644 .changeset/emoji-server-checks.md create mode 100644 .changeset/friendly-dashboard-pages.md create mode 100644 .changeset/minecraft-account-sync.md create mode 100644 .changeset/permanent-profile-emojis.md create mode 100644 .changeset/private-account-settings.md create mode 100644 .changeset/sidebar-account-menu.md create mode 100644 drizzle/0001_fresh_sally_floyd.sql create mode 100644 drizzle/meta/0001_snapshot.json create mode 100644 src/lib/components/dashboard/EmojiEditor.svelte create mode 100644 src/lib/components/dashboard/EmojiEditor.svelte.spec.ts create mode 100644 src/lib/hooks/use-frecency.svelte.ts create mode 100644 src/lib/server/account-data.db.spec.ts create mode 100644 src/lib/server/account-data.ts create mode 100644 src/lib/server/db/schema/discord.schema.ts create mode 100644 src/lib/server/db/schema/minecraft.schema.ts create mode 100644 src/lib/server/db/schema/patreon.schema.ts create mode 100644 src/lib/server/db/schema/profile-emoji.schema.ts create mode 100644 src/lib/server/discord-profile.spec.ts create mode 100644 src/lib/server/discord-profile.ts create mode 100644 src/lib/server/minecraft-accounts.spec.ts create mode 100644 src/lib/server/minecraft-accounts.ts create mode 100644 src/lib/server/patreon-cooldown.spec.ts create mode 100644 src/lib/server/patreon-cooldown.ts create mode 100644 src/lib/server/patreon-entitlements.ts create mode 100644 src/lib/server/patreon.db.spec.ts create mode 100644 src/lib/server/patreon.spec.ts create mode 100644 src/lib/server/patreon.ts create mode 100644 src/lib/server/profile-emoji.db.spec.ts create mode 100644 src/lib/server/profile-emoji.spec.ts create mode 100644 src/lib/server/profile-emoji.ts create mode 100644 src/lib/shared/api/patreon-generated.ts create mode 100644 src/lib/shared/profile-emoji.remote.spec.ts create mode 100644 src/lib/shared/profile-emoji.remote.ts create mode 100644 src/lib/shared/profile-emoji.spec.ts create mode 100644 src/lib/shared/profile-emoji.ts create mode 100644 src/routes/(protected)/dashboard/+layout.svelte create mode 100644 src/routes/(protected)/dashboard/dashboard-sidebar.svelte create mode 100644 src/routes/(protected)/dashboard/dashboard.remote.spec.ts create mode 100644 src/routes/(protected)/dashboard/dashboard.remote.ts create mode 100644 src/routes/(protected)/dashboard/dashboard.svelte.spec.ts create mode 100644 src/routes/(protected)/dashboard/donator-perks/+page.svelte create mode 100644 src/routes/(protected)/dashboard/donator-perks/perks.remote.ts create mode 100644 src/routes/(protected)/dashboard/donator-perks/perks.svelte.spec.ts create mode 100644 src/routes/(protected)/dashboard/minecraft-accounts.svelte create mode 100644 src/routes/(protected)/dashboard/minecraft-accounts.svelte.spec.ts create mode 100644 src/routes/(protected)/dashboard/minecraft.remote.spec.ts create mode 100644 src/routes/(protected)/dashboard/minecraft.remote.ts create mode 100644 src/routes/(protected)/dashboard/settings/+page.svelte create mode 100644 src/routes/(protected)/dashboard/settings/session-device.spec.ts create mode 100644 src/routes/(protected)/dashboard/settings/session-device.ts create mode 100644 src/routes/(protected)/dashboard/settings/settings.remote.spec.ts create mode 100644 src/routes/(protected)/dashboard/settings/settings.remote.ts create mode 100644 src/routes/(protected)/dashboard/settings/settings.svelte.spec.ts delete mode 100644 src/routes/login/login.remote.ts create mode 100644 src/test-utils/EmojiPickerTest.svelte diff --git a/.changeset/discord-profile-assets.md b/.changeset/discord-profile-assets.md new file mode 100644 index 000000000..6319feceb --- /dev/null +++ b/.changeset/discord-profile-assets.md @@ -0,0 +1,5 @@ +--- +"skycrypt-frontend": minor +--- + +Store Discord username, primary guild, and avatar, banner, and avatar decoration hashes in an application-owned table, refresh them after successful Discord sign-in, and expose them through the custom session. diff --git a/.changeset/emoji-picker-performance.md b/.changeset/emoji-picker-performance.md new file mode 100644 index 000000000..e2c7f0111 --- /dev/null +++ b/.changeset/emoji-picker-performance.md @@ -0,0 +1,7 @@ +--- +"skycrypt-frontend": patch +--- + +Render emojis progressively and cache search terms to reduce emoji picker opening and search work while preserving substring search and allowed emoji restrictions. + +Initialize the profile emoji picker inside its popover so the first emoji is highlighted and its footer preview appears when opening. diff --git a/.changeset/emoji-server-checks.md b/.changeset/emoji-server-checks.md new file mode 100644 index 000000000..7187c6cfb --- /dev/null +++ b/.changeset/emoji-server-checks.md @@ -0,0 +1,5 @@ +--- +"skycrypt-frontend": patch +--- + +Validate profile emoji inputs in the server save helper and check stored entitlement levels before displaying emojis. Reject unsupported entitlement levels. diff --git a/.changeset/friendly-dashboard-pages.md b/.changeset/friendly-dashboard-pages.md new file mode 100644 index 000000000..406a50262 --- /dev/null +++ b/.changeset/friendly-dashboard-pages.md @@ -0,0 +1,7 @@ +--- +"skycrypt-frontend": minor +--- + +Complete the dashboard with account linking, profile and session settings, and Patreon membership details. Move dashboard loading and membership verification to authenticated remote functions. + +Refine dashboard navigation, account connections, donator benefit previews, and mobile session settings. diff --git a/.changeset/minecraft-account-sync.md b/.changeset/minecraft-account-sync.md new file mode 100644 index 000000000..f5664ce58 --- /dev/null +++ b/.changeset/minecraft-account-sync.md @@ -0,0 +1,5 @@ +--- +"skycrypt-frontend": minor +--- + +Store Minecraft accounts returned by MC-ID separately from authentication accounts. Sync UUIDs, usernames, and primary-account status after linking, and add a dashboard sync action with token refresh and reconnect support. diff --git a/.changeset/permanent-profile-emojis.md b/.changeset/permanent-profile-emojis.md new file mode 100644 index 000000000..864268eb6 --- /dev/null +++ b/.changeset/permanent-profile-emojis.md @@ -0,0 +1,5 @@ +--- +"skycrypt-frontend": minor +--- + +Let verified Minecraft accounts choose and save permanent profile emojis from Patreon support or existing UUID grants. Add colored heart selection, a searchable emoji picker with skin tones, custom emoji validation, profile previews, and emoji display on stats pages. Refresh the donator perks page, correct donation amounts to USD, automatically check support after linking Patreon, and store the verification countdown in localStorage while enforcing the rate limit in server memory. diff --git a/.changeset/private-account-settings.md b/.changeset/private-account-settings.md new file mode 100644 index 000000000..36f175864 --- /dev/null +++ b/.changeset/private-account-settings.md @@ -0,0 +1,5 @@ +--- +"skycrypt-frontend": minor +--- + +Remove profile editing from settings, improve the active sessions list, and add personal data export and account deletion. diff --git a/.changeset/sidebar-account-menu.md b/.changeset/sidebar-account-menu.md new file mode 100644 index 000000000..c474e6da1 --- /dev/null +++ b/.changeset/sidebar-account-menu.md @@ -0,0 +1,5 @@ +--- +"skycrypt-frontend": minor +--- + +Add an avatar and account menu to the dashboard sidebar footer with account settings and sign-out actions. diff --git a/.env.example b/.env.example index a4b2638b5..898921242 100644 --- a/.env.example +++ b/.env.example @@ -39,6 +39,12 @@ MC_ID_CLIENT_SECRET="" # https://www.patreon.com/portal/registration/register-clients PATREON_CLIENT_ID="" PATREON_CLIENT_SECRET="" +# Numeric campaign ID from the Patreon campaign URL/API response +PATREON_CAMPAIGN_ID="" +PATREON_CREATOR_ACCESS_TOKEN="" +PATREON_CREATOR_REFRESH_TOKEN="" +PUBLIC_PATREON_COLORED_HEART_CENTS="500" +PUBLIC_PATREON_ANY_EMOJI_CENTS="6000" # Payload CMS PUBLIC_CMS_URL="https://cms.shiiyu.moe" @@ -51,4 +57,4 @@ PUBLIC_UMAMI_WEBSITE_ID="" PUBLIC_UMAMI_SCRIPT_URL="" PUBLIC_UMAMI_RECORDER_URL="" PUBLIC_UMAMI_HOST_URL="" -PUBLIC_UMAMI_ENABLE_HEATMAPS="false" \ No newline at end of file +PUBLIC_UMAMI_ENABLE_HEATMAPS="false" diff --git a/drizzle/0001_fresh_sally_floyd.sql b/drizzle/0001_fresh_sally_floyd.sql new file mode 100644 index 000000000..4c1fb3fed --- /dev/null +++ b/drizzle/0001_fresh_sally_floyd.sql @@ -0,0 +1,61 @@ +CREATE TABLE "discord_profile" ( + "user_id" text PRIMARY KEY NOT NULL, + "discord_user_id" text NOT NULL, + "username" text, + "primary_guild" jsonb, + "avatar_hash" text, + "banner_hash" text, + "avatar_decoration_hash" text, + "synced_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "discord_profile_discord_user_id_unique" UNIQUE("discord_user_id") +); +--> statement-breakpoint +CREATE TABLE "patreon_creator_token" ( + "campaign_id" text PRIMARY KEY NOT NULL, + "access_token" text NOT NULL, + "refresh_token" text NOT NULL, + "expires_at" timestamp, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "patreon_membership" ( + "id" text PRIMARY KEY NOT NULL, + "user_id" text NOT NULL, + "patreon_user_id" text NOT NULL, + "member_id" text NOT NULL, + "campaign_id" text NOT NULL, + "lifetime_support_cents" integer DEFAULT 0 NOT NULL, + "currently_entitled_amount_cents" integer DEFAULT 0 NOT NULL, + "patron_status" text, + "entitlement_level" integer DEFAULT 0 NOT NULL, + "verified_at" timestamp DEFAULT now() NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "minecraft_account" ( + "account_id" text NOT NULL, + "uuid" text NOT NULL, + "username" text NOT NULL, + "primary" boolean NOT NULL, + "synced_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "minecraft_account_account_id_uuid_pk" PRIMARY KEY("account_id","uuid") +); +--> statement-breakpoint +CREATE TABLE "profile_emoji" ( + "uuid" text PRIMARY KEY NOT NULL, + "emoji" text NOT NULL, + "entitlement_level" integer DEFAULT 1 NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "profile_emoji_entitlement_check" CHECK ("profile_emoji"."entitlement_level" between 1 and 2) +); +--> statement-breakpoint +ALTER TABLE "apikey" ALTER COLUMN "enabled" SET DEFAULT false;--> statement-breakpoint +ALTER TABLE "apikey" ALTER COLUMN "rate_limit_time_window" SET DEFAULT 300000;--> statement-breakpoint +ALTER TABLE "apikey" ALTER COLUMN "rate_limit_max" SET DEFAULT 300;--> statement-breakpoint +ALTER TABLE "discord_profile" ADD CONSTRAINT "discord_profile_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "patreon_membership" ADD CONSTRAINT "patreon_membership_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "minecraft_account" ADD CONSTRAINT "minecraft_account_account_id_account_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."account"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "patreon_membership_userId_uidx" ON "patreon_membership" USING btree ("user_id");--> statement-breakpoint +CREATE UNIQUE INDEX "patreon_membership_memberId_uidx" ON "patreon_membership" USING btree ("member_id");--> statement-breakpoint +CREATE INDEX "patreon_membership_patreonUserId_idx" ON "patreon_membership" USING btree ("patreon_user_id"); \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 000000000..f6e68ea37 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,991 @@ +{ + "id": "76c26c9d-f667-4c55-8934-fdb1ae060a64", + "prevId": "38215b0e-303d-4a3b-b06f-661d7774eeb7", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apikey": { + "name": "apikey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "config_id": { + "name": "config_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start": { + "name": "start", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "prefix": { + "name": "prefix", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refill_interval": { + "name": "refill_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "refill_amount": { + "name": "refill_amount", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "rate_limit_enabled": { + "name": "rate_limit_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": true + }, + "rate_limit_time_window": { + "name": "rate_limit_time_window", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 300000 + }, + "rate_limit_max": { + "name": "rate_limit_max", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 300 + }, + "request_count": { + "name": "request_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "remaining": { + "name": "remaining", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_request": { + "name": "last_request", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "apikey_configId_idx": { + "name": "apikey_configId_idx", + "columns": [ + { + "expression": "config_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_referenceId_idx": { + "name": "apikey_referenceId_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "apikey_key_idx": { + "name": "apikey_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "impersonated_by": { + "name": "impersonated_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banned": { + "name": "banned", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "ban_reason": { + "name": "ban_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "ban_expires": { + "name": "ban_expires", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.discord_profile": { + "name": "discord_profile", + "schema": "", + "columns": { + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "discord_user_id": { + "name": "discord_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "primary_guild": { + "name": "primary_guild", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "avatar_hash": { + "name": "avatar_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "banner_hash": { + "name": "banner_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_decoration_hash": { + "name": "avatar_decoration_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "discord_profile_user_id_user_id_fk": { + "name": "discord_profile_user_id_user_id_fk", + "tableFrom": "discord_profile", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "discord_profile_discord_user_id_unique": { + "name": "discord_profile_discord_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "discord_user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.patreon_creator_token": { + "name": "patreon_creator_token", + "schema": "", + "columns": { + "campaign_id": { + "name": "campaign_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.patreon_membership": { + "name": "patreon_membership", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "patreon_user_id": { + "name": "patreon_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "member_id": { + "name": "member_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "campaign_id": { + "name": "campaign_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lifetime_support_cents": { + "name": "lifetime_support_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "currently_entitled_amount_cents": { + "name": "currently_entitled_amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "patron_status": { + "name": "patron_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "entitlement_level": { + "name": "entitlement_level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "patreon_membership_userId_uidx": { + "name": "patreon_membership_userId_uidx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "patreon_membership_memberId_uidx": { + "name": "patreon_membership_memberId_uidx", + "columns": [ + { + "expression": "member_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "patreon_membership_patreonUserId_idx": { + "name": "patreon_membership_patreonUserId_idx", + "columns": [ + { + "expression": "patreon_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "patreon_membership_user_id_user_id_fk": { + "name": "patreon_membership_user_id_user_id_fk", + "tableFrom": "patreon_membership", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.minecraft_account": { + "name": "minecraft_account", + "schema": "", + "columns": { + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "primary": { + "name": "primary", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "synced_at": { + "name": "synced_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "minecraft_account_account_id_account_id_fk": { + "name": "minecraft_account_account_id_account_id_fk", + "tableFrom": "minecraft_account", + "tableTo": "account", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "minecraft_account_account_id_uuid_pk": { + "name": "minecraft_account_account_id_uuid_pk", + "columns": [ + "account_id", + "uuid" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.profile_emoji": { + "name": "profile_emoji", + "schema": "", + "columns": { + "uuid": { + "name": "uuid", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "emoji": { + "name": "emoji", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entitlement_level": { + "name": "entitlement_level", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "profile_emoji_entitlement_check": { + "name": "profile_emoji_entitlement_check", + "value": "\"profile_emoji\".\"entitlement_level\" between 1 and 2" + } + }, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 4a7cf04a6..4bfc2f7ff 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1779478193206, "tag": "0000_dapper_killer_shrike", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1791664895171, + "tag": "0001_fresh_sally_floyd", + "breakpoints": true } ] } \ No newline at end of file diff --git a/orval.config.ts b/orval.config.ts index c8acb9799..1b6df392f 100644 --- a/orval.config.ts +++ b/orval.config.ts @@ -46,5 +46,16 @@ export default defineConfig({ hooks: { afterAllFilesWrite: "oxfmt ./src/lib/shared/api/cms-generated.ts" } + }, + patreon: { + input: "https://raw.githubusercontent.com/ghostrider-05/patreon-api-spec/refs/heads/main/specs/openapi.json", + output: { + target: "./src/lib/shared/api/patreon-generated.ts", + client: "fetch", + tsconfig: "./tsconfig.json" + }, + hooks: { + afterAllFilesWrite: "oxfmt ./src/lib/shared/api/patreon-generated.ts" + } } }); diff --git a/package.json b/package.json index 77a997ec5..e5960c07e 100644 --- a/package.json +++ b/package.json @@ -36,20 +36,20 @@ "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", "db:studio": "drizzle-kit studio", - "auth:schema": "auth generate --config src/lib/server/auth.ts --output src/lib/server/db/schema/auth.schema.ts --yes", + "auth:schema": "auth generate --config src/lib/server/auth.ts --output src/lib/server/db/schema/auth.schema.ts --yes && oxfmt --write src/lib/server/db/schema/auth.schema.ts", "update:shadcn": "pnpm dlx shadcn-svelte@latest update -a && oxfmt --write ./src/lib/components/ui && oxfmt --write ./src/lib/shared/utils.ts" }, "dependencies": { - "@payloadcms/richtext-lexical": "^3.88.0", - "@sentry/sveltekit": "^10.70.0", - "@takumi-rs/core-linux-x64-gnu": "^2.10.0", + "@payloadcms/richtext-lexical": "^3.89.0", + "@sentry/sveltekit": "^10.74.0", + "@takumi-rs/core-linux-x64-gnu": "^2.13.7", "culori": "^4.0.2", - "simple-git-hooks": "^2.13.1", - "takumi-js": "^2.10.0" + "simple-git-hooks": "^2.14.0", + "takumi-js": "^2.13.7" }, "devDependencies": { - "@better-auth/api-key": "^1.6.29", - "@changesets/cli": "^3.0.1", + "@better-auth/api-key": "^1.7.7", + "@changesets/cli": "^3.0.2", "@commitlint/cli": "^21.2.2", "@commitlint/config-conventional": "^21.2.2", "@commitlint/types": "^21.2.0", @@ -58,14 +58,15 @@ "@dnd-kit/dom": "^0.5.0", "@dnd-kit/helpers": "^0.5.0", "@dnd-kit/svelte": "^0.5.0", + "@emoji-mart/data": "^1.2.1", "@eslint/js": "^10.0.1", "@fontsource-variable/geist-mono": "^5.3.0", "@fontsource-variable/montserrat": "^5.3.0", "@fontsource/unifontex": "^5.3.0", - "@internationalized/date": "^3.12.3", - "@lucide/svelte": "^1.33.0", + "@internationalized/date": "^3.12.4", + "@lucide/svelte": "^1.54.0", "@oslojs/encoding": "^1.1.0", - "@playwright/test": "^1.62.1", + "@playwright/test": "^1.63.0", "@selemondev/svgl-svelte": "^2.17.0", "@sveltejs/adapter-node": "^5.5.7", "@sveltejs/kit": "^2.70.3", @@ -74,48 +75,49 @@ "@tailwindcss/vite": "^4.3.3", "@types/culori": "^4.0.1", "@types/eslint": "^9.6.1", - "@types/node": "^26.2.0", + "@types/node": "^26.5.1", "@types/relaxed-json": "^1.0.4", "@types/umami-browser": "^2.18.1", "@types/upng-js": "^2.1.5", "@vitest/browser-playwright": "^4.1.11", "@vitest/coverage-v8": "^4.1.11", "@vitest/ui": "^4.1.11", - "auth": "^1.6.29", - "better-auth": "^1.6.29", - "bits-ui": "^2.19.0", + "auth": "^1.7.7", + "better-auth": "^1.7.7", + "bits-ui": "^2.19.2", "changesets-changelog-clean": "^1.4.0", "clsx": "^2.1.1", "date-fns": "^4.4.0", - "devalue": "^5.9.1", + "devalue": "^5.9.2", "discord-api-types": "^0.38.55", "dotenv-cli": "^11.0.0", - "drizzle-kit": "^0.31.10", - "drizzle-orm": "^0.45.2", - "eslint": "^10.8.1", + "drizzle-kit": "^0.31.11", + "drizzle-orm": "^0.45.4", + "eslint": "^10.10.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-svelte": "^3.23.0", "formsnap": "^2.0.1", - "globals": "^17.11.0", - "ky": "^2.0.2", + "globals": "^17.12.0", + "ky": "^2.1.0", "mode-watcher": "^1.1.0", "numerable": "^0.3.15", - "orval": "^8.24.0", - "oxfmt": "^0.64.0", + "orval": "^8.31.0", + "oxfmt": "^0.67.0", "paneforge": "^1.0.2", - "playwright": "^1.62.1", + "playwright": "^1.63.0", "postgres": "^3.4.9", - "pretty-ms": "^9.3.0", + "pretty-ms": "^9.3.1", "runed": "^0.37.1", - "shadcn-svelte": "^1.5.0", + "shadcn-svelte": "^1.6.1", "skinview3d": "^3.4.2", "super-sitemap": "^2.0.6", - "svelte": "^5.56.10", + "svelte": "^5.57.0", "svelte-check": "^4.7.6", "svelte-persisted-store": "^0.12.0", "svelte-seo": "^2.0.0", "svelte-sonner": "^1.2.1", "svelte-tiny-virtual-list": "4.0.0", + "svelte-toolbelt": "^0.10.6", "sveltekit-superforms": "^2.30.2", "tailwind-merge": "^3.6.0", "tailwind-variants": "^3.3.1", @@ -124,12 +126,12 @@ "tslib": "^2.8.1", "tw-animate-css": "^1.4.0", "typescript": "^6.0.3", - "typescript-eslint": "^8.67.0", + "typescript-eslint": "^8.70.0", "vaul-svelte": "1.0.0-next.7", - "vite": "^8.2.2", + "vite": "^8.3.0", "vitest": "^4.1.11", - "vitest-browser-svelte": "^3.0.0", - "zod": "^4.4.3" + "vitest-browser-svelte": "^3.1.0", + "zod": "^4.6.2" }, "simple-git-hooks": { "commit-msg": "pnpm exec commitlint --edit $1" @@ -139,7 +141,7 @@ "deno": "forbidden, use node instead", "node": "^24", "npm": "forbidden, use pnpm instead", - "pnpm": "^11", + "pnpm": "^12", "yarn": "forbidden, use pnpm instead" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a200fce1d..c9d81a51a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,33 +9,33 @@ importers: .: dependencies: '@payloadcms/richtext-lexical': - specifier: ^3.88.0 - version: 3.88.0(@faceless-ui/modal@3.0.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@faceless-ui/scroll-info@2.0.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@payloadcms/next@3.88.0(@types/react@19.2.16)(graphql@16.14.1)(monaco-editor@0.55.1)(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(payload@3.88.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@7.2.0)(typescript@6.0.3))(@types/react@19.2.16)(monaco-editor@0.55.1)(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(payload@3.88.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@7.2.0)(typescript@6.0.3)(yjs@13.6.31) + specifier: ^3.89.0 + version: 3.89.0(@faceless-ui/modal@3.0.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@faceless-ui/scroll-info@2.0.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@payloadcms/next@3.89.0(@types/react@19.2.16)(graphql@16.14.1)(monaco-editor@0.55.1)(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.63.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(payload@3.89.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@7.2.0)(typescript@6.0.3))(@types/react@19.2.16)(monaco-editor@0.55.1)(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.63.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(payload@3.89.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@7.2.0)(typescript@6.0.3)(yjs@13.6.31) '@sentry/sveltekit': - specifier: ^10.70.0 - version: 10.70.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(rollup@4.62.4)(supports-color@7.2.0)(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) + specifier: ^10.74.0 + version: 10.74.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(rollup@4.63.1)(supports-color@7.2.0)(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) '@takumi-rs/core-linux-x64-gnu': - specifier: ^2.10.0 - version: 2.10.0 + specifier: ^2.13.7 + version: 2.13.7 culori: specifier: ^4.0.2 version: 4.0.2 simple-git-hooks: - specifier: ^2.13.1 - version: 2.13.1 + specifier: ^2.14.0 + version: 2.14.0 takumi-js: - specifier: ^2.10.0 - version: 2.10.0(csstype@3.2.3)(react@19.2.7) + specifier: ^2.13.7 + version: 2.13.7(csstype@3.2.3)(react@19.2.7) devDependencies: '@better-auth/api-key': - specifier: ^1.6.29 - version: 1.6.29(04ce92fbd93057ce4052b8a6f33b15c6) + specifier: ^1.7.7 + version: 1.7.7(2876af4a9b015b644355db9021441558) '@changesets/cli': - specifier: ^3.0.1 - version: 3.0.1 + specifier: ^3.0.2 + version: 3.0.2 '@commitlint/cli': specifier: ^21.2.2 - version: 21.2.2(@types/node@26.2.0)(conventional-commits-parser@7.1.2)(typescript@6.0.3) + version: 21.2.2(@types/node@26.5.1)(conventional-commits-parser@7.1.2)(typescript@6.0.3) '@commitlint/config-conventional': specifier: ^21.2.2 version: 21.2.2 @@ -56,10 +56,13 @@ importers: version: 0.5.0 '@dnd-kit/svelte': specifier: ^0.5.0 - version: 0.5.0(svelte@5.56.10(@typescript-eslint/types@8.67.0)) + version: 0.5.0(svelte@5.57.0(@typescript-eslint/types@8.70.0)) + '@emoji-mart/data': + specifier: ^1.2.1 + version: 1.2.1 '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0)) + version: 10.0.1(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0)) '@fontsource-variable/geist-mono': specifier: ^5.3.0 version: 5.3.0 @@ -70,35 +73,35 @@ importers: specifier: ^5.3.0 version: 5.3.0 '@internationalized/date': - specifier: ^3.12.3 - version: 3.12.3 + specifier: ^3.12.4 + version: 3.12.4 '@lucide/svelte': - specifier: ^1.33.0 - version: 1.33.0(svelte@5.56.10(@typescript-eslint/types@8.67.0)) + specifier: ^1.54.0 + version: 1.54.0(svelte@5.57.0(@typescript-eslint/types@8.70.0)) '@oslojs/encoding': specifier: ^1.1.0 version: 1.1.0 '@playwright/test': - specifier: ^1.62.1 - version: 1.62.1 + specifier: ^1.63.0 + version: 1.63.0 '@selemondev/svgl-svelte': specifier: ^2.17.0 - version: 2.17.0(svelte@5.56.10(@typescript-eslint/types@8.67.0)) + version: 2.17.0(svelte@5.57.0(@typescript-eslint/types@8.70.0)) '@sveltejs/adapter-node': specifier: ^5.5.7 - version: 5.5.7(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0))) + version: 5.5.7(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0))) '@sveltejs/kit': specifier: ^2.70.3 - version: 2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) + version: 2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) '@sveltejs/load-config': specifier: ^0.2.3 version: 0.2.3 '@sveltejs/vite-plugin-svelte': specifier: ^7.3.0 - version: 7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) + version: 7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) '@tailwindcss/vite': specifier: ^4.3.3 - version: 4.3.3(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.3.3(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) '@types/culori': specifier: ^4.0.1 version: 4.0.1 @@ -106,8 +109,8 @@ importers: specifier: ^9.6.1 version: 9.6.1 '@types/node': - specifier: ^26.2.0 - version: 26.2.0 + specifier: ^26.5.1 + version: 26.5.1 '@types/relaxed-json': specifier: ^1.0.4 version: 1.0.4 @@ -119,7 +122,7 @@ importers: version: 2.1.5 '@vitest/browser-playwright': specifier: ^4.1.11 - version: 4.1.11(playwright@1.62.1)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.11) + version: 4.1.11(playwright@1.63.0)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.11) '@vitest/coverage-v8': specifier: ^4.1.11 version: 4.1.11(@vitest/browser@4.1.11)(vitest@4.1.11) @@ -127,14 +130,14 @@ importers: specifier: ^4.1.11 version: 4.1.11(vitest@4.1.11) auth: - specifier: ^1.6.29 - version: 1.6.29(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(better-call@1.4.0(zod@4.4.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(kysely@0.29.5)(postgres@3.4.9))(jose@6.2.8)(kysely@0.29.5)(magicast@0.5.4)(nanostores@1.4.2)(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@7.2.0)(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vitest@4.1.11) + specifier: ^1.7.7 + version: 1.7.7(3b94b67bc64ad3733f525f5ab8710cfe) better-auth: - specifier: ^1.6.29 - version: 1.6.29(@opentelemetry/api@1.9.1)(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(kysely@0.29.5)(postgres@3.4.9))(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vitest@4.1.11) + specifier: ^1.7.7 + version: 1.7.7(@opentelemetry/api@1.9.1)(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(drizzle-kit@0.31.11)(drizzle-orm@0.45.4(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(kysely@0.29.5)(postgres@3.4.9))(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.63.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vitest@4.1.11) bits-ui: - specifier: ^2.19.0 - version: 2.19.0(@internationalized/date@3.12.3)(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0)) + specifier: ^2.19.2 + version: 2.19.2(@internationalized/date@3.12.4)(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0)) changesets-changelog-clean: specifier: ^1.4.0 version: 1.4.0 @@ -145,8 +148,8 @@ importers: specifier: ^4.4.0 version: 4.4.0 devalue: - specifier: ^5.9.1 - version: 5.9.1 + specifier: ^5.9.2 + version: 5.9.2 discord-api-types: specifier: ^0.38.55 version: 0.38.55 @@ -154,59 +157,59 @@ importers: specifier: ^11.0.0 version: 11.0.0 drizzle-kit: - specifier: ^0.31.10 - version: 0.31.10 + specifier: ^0.31.11 + version: 0.31.11 drizzle-orm: - specifier: ^0.45.2 - version: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(kysely@0.29.5)(postgres@3.4.9) + specifier: ^0.45.4 + version: 0.45.4(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(kysely@0.29.5)(postgres@3.4.9) eslint: - specifier: ^10.8.1 - version: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + specifier: ^10.10.0 + version: 10.10.0(jiti@2.7.0)(supports-color@7.2.0) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0)) + version: 10.1.8(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0)) eslint-plugin-svelte: specifier: ^3.23.0 - version: 3.23.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(svelte@5.56.10(@typescript-eslint/types@8.67.0)) + version: 3.23.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(svelte@5.57.0(@typescript-eslint/types@8.70.0)) formsnap: specifier: ^2.0.1 - version: 2.0.1(svelte@5.56.10(@typescript-eslint/types@8.67.0))(sveltekit-superforms@2.30.2(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(@types/json-schema@7.0.15)(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)) + version: 2.0.1(svelte@5.57.0(@typescript-eslint/types@8.70.0))(sveltekit-superforms@2.30.2(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(@types/json-schema@7.0.15)(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)) globals: - specifier: ^17.11.0 - version: 17.11.0 + specifier: ^17.12.0 + version: 17.12.0 ky: - specifier: ^2.0.2 - version: 2.0.2 + specifier: ^2.1.0 + version: 2.1.0 mode-watcher: specifier: ^1.1.0 - version: 1.1.0(svelte@5.56.10(@typescript-eslint/types@8.67.0)) + version: 1.1.0(svelte@5.57.0(@typescript-eslint/types@8.70.0)) numerable: specifier: ^0.3.15 version: 0.3.15 orval: - specifier: ^8.24.0 - version: 8.24.0(prettier@3.9.6)(supports-color@7.2.0)(typescript@6.0.3) + specifier: ^8.31.0 + version: 8.31.0(effect@3.22.1)(prettier@3.9.6)(supports-color@7.2.0)(typescript@6.0.3)(zod@4.6.2) oxfmt: - specifier: ^0.64.0 - version: 0.64.0(svelte@5.56.10(@typescript-eslint/types@8.67.0)) + specifier: ^0.67.0 + version: 0.67.0(svelte@5.57.0(@typescript-eslint/types@8.70.0)) paneforge: specifier: ^1.0.2 - version: 1.0.2(svelte@5.56.10(@typescript-eslint/types@8.67.0)) + version: 1.0.2(svelte@5.57.0(@typescript-eslint/types@8.70.0)) playwright: - specifier: ^1.62.1 - version: 1.62.1 + specifier: ^1.63.0 + version: 1.63.0 postgres: specifier: ^3.4.9 version: 3.4.9 pretty-ms: - specifier: ^9.3.0 - version: 9.3.0 + specifier: ^9.3.1 + version: 9.3.1 runed: specifier: ^0.37.1 - version: 0.37.1(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(zod@4.4.3) + version: 0.37.1(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(zod@4.6.2) shadcn-svelte: - specifier: ^1.5.0 - version: 1.5.0(svelte@5.56.10(@typescript-eslint/types@8.67.0)) + specifier: ^1.6.1 + version: 1.6.1(svelte@5.57.0(@typescript-eslint/types@8.70.0)) skinview3d: specifier: ^3.4.2 version: 3.4.2 @@ -214,26 +217,29 @@ importers: specifier: ^2.0.6 version: 2.0.6 svelte: - specifier: ^5.56.10 - version: 5.56.10(@typescript-eslint/types@8.67.0) + specifier: ^5.57.0 + version: 5.57.0(@typescript-eslint/types@8.70.0) svelte-check: specifier: ^4.7.6 - version: 4.7.6(picomatch@4.0.5)(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3) + version: 4.7.6(picomatch@4.0.7)(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3) svelte-persisted-store: specifier: ^0.12.0 - version: 0.12.0(svelte@5.56.10(@typescript-eslint/types@8.67.0)) + version: 0.12.0(svelte@5.57.0(@typescript-eslint/types@8.70.0)) svelte-seo: specifier: ^2.0.0 - version: 2.0.0(svelte@5.56.10(@typescript-eslint/types@8.67.0)) + version: 2.0.0(svelte@5.57.0(@typescript-eslint/types@8.70.0)) svelte-sonner: specifier: ^1.2.1 - version: 1.2.1(svelte@5.56.10(@typescript-eslint/types@8.67.0)) + version: 1.2.1(svelte@5.57.0(@typescript-eslint/types@8.70.0)) svelte-tiny-virtual-list: specifier: 4.0.0 - version: 4.0.0(svelte@5.56.10(@typescript-eslint/types@8.67.0)) + version: 4.0.0(svelte@5.57.0(@typescript-eslint/types@8.70.0)) + svelte-toolbelt: + specifier: ^0.10.6 + version: 0.10.6(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0)) sveltekit-superforms: specifier: ^2.30.2 - version: 2.30.2(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(@types/json-schema@7.0.15)(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3) + version: 2.30.2(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(@types/json-schema@7.0.15)(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3) tailwind-merge: specifier: ^3.6.0 version: 3.6.0 @@ -256,23 +262,23 @@ importers: specifier: ^6.0.3 version: 6.0.3 typescript-eslint: - specifier: ^8.67.0 - version: 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + specifier: ^8.70.0 + version: 8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) vaul-svelte: specifier: 1.0.0-next.7 - version: 1.0.0-next.7(svelte@5.56.10(@typescript-eslint/types@8.67.0)) + version: 1.0.0-next.7(svelte@5.57.0(@typescript-eslint/types@8.70.0)) vite: - specifier: ^8.2.2 - version: 8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0) + specifier: ^8.3.0 + version: 8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0) vitest: specifier: ^4.1.11 - version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.2.0)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.11.2)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) + version: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.5.1)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.12.0)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) vitest-browser-svelte: - specifier: ^3.0.0 - version: 3.0.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vitest@4.1.11) + specifier: ^3.1.0 + version: 3.1.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vitest@4.1.11) zod: - specifier: ^4.4.3 - version: 4.4.3 + specifier: ^4.6.2 + version: 4.6.2 packages: @@ -280,17 +286,6 @@ packages: resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==} engines: {node: '>= 16'} - '@apm-js-collab/code-transformer-bundler-plugins@0.7.4': - resolution: {integrity: sha512-nAfOeZPSUAQvJa1iFT/5oCrTm5YQhMMrfCNthNnaXHZiOQhu1KGuLoIx7HtbAi3wfwaBYLaICPIeenIaEwcXIg==} - engines: {node: '>=18.0.0'} - - '@apm-js-collab/code-transformer@0.18.1': - resolution: {integrity: sha512-u1Hb6bHjWtkSpiprwVP6YaHC1DTN4RAU3zYkUDUe7WMnJwdyU1pwTL9dFKiSJB9IiLue/EQovmyx6xhU7FFtAQ==} - hasBin: true - - '@apm-js-collab/tracing-hooks@0.13.0': - resolution: {integrity: sha512-mTvWz9rnQwx1U3h0XPTHaX7bgfkpipLLTQyjlC2cdhQpQEuoLT0AGzoydeoq2NxfEVv6fWOOETcSbb2nptleyw==} - '@ark/schema@0.56.2': resolution: {integrity: sha512-Qx4D2JFbBWpntiHZaTv7bGG4H/M2rigiknezKg/WVyDSaLdE4YCcWAOoFB7pjjDqHbbV2OqRfntm1nnXvwMexg==} @@ -464,71 +459,68 @@ packages: resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@better-auth/api-key@1.6.29': - resolution: {integrity: sha512-JNV7Zxu9KAfQs3lAnUV5oXJUveNuGbmOIDhk1BffoHvCFUWBRM2UaqAgvsz483OdL9eqhgWL1w93m0WS/rLlKA==} + '@better-auth/api-key@1.7.7': + resolution: {integrity: sha512-XWtZxF37A7BURHh/GvsYh2Vype1p+R1ZxvQHkRzB4tK3uniZpILe2WRIXfVj3m8NIBHdg8Ez8S8sZJY/ewTu7w==} peerDependencies: - '@better-auth/core': ^1.6.29 + '@better-auth/core': ^1.7.7 '@better-auth/utils': 0.4.2 - better-auth: ^1.6.29 + better-auth: ^1.7.7 better-call: 1.4.0 - '@better-auth/core@1.6.29': - resolution: {integrity: sha512-hkUxePo548G0Y247had36TcfsH7E+cofcTc2UivMAbjkdLPKn1ZUk6Pjkc/kvk9xTbWYCUDX6JA9emJc0Gnufg==} + '@better-auth/core@1.7.7': + resolution: {integrity: sha512-srgFzHEjB2WWlokFz1i4BfG41sNhsA105Pukgqu6RrEh5A+LSkzf1mCHydVipkjt9l5qswtBMi7IL6ouHbZL4Q==} peerDependencies: '@better-auth/utils': 0.4.2 - '@better-fetch/fetch': 1.3.1 - '@cloudflare/workers-types': '>=4' + '@better-fetch/fetch': 1.3.2 '@opentelemetry/api': ^1.9.0 better-call: 1.4.0 jose: ^6.1.0 kysely: ^0.28.5 || ^0.29.0 nanostores: ^1.0.1 peerDependenciesMeta: - '@cloudflare/workers-types': - optional: true '@opentelemetry/api': optional: true - '@better-auth/drizzle-adapter@1.6.29': - resolution: {integrity: sha512-IdvrqHnnRNKqz6KOSII3rZNiTzv1pmVmx5xGRGZdVm8W3OYi98an5kIovao/b3GZh2nHGTzdeTfMSSZNCyGXBg==} + '@better-auth/drizzle-adapter@1.7.7': + resolution: {integrity: sha512-qon0U94PR5FQysuyGoAlGVQpIIiNg5dtDpwjE63sN/HjoRWZadZzHPcS9mn7JsJyQZK6iTNhd7LD0AaXt2f78w==} peerDependencies: - '@better-auth/core': ^1.6.29 + '@better-auth/core': ^1.7.7 '@better-auth/utils': 0.4.2 - drizzle-orm: ^0.45.2 + drizzle-orm: ^0.45.2 || >=1.0.0-rc.1 <2.0.0 peerDependenciesMeta: drizzle-orm: optional: true - '@better-auth/kysely-adapter@1.6.29': - resolution: {integrity: sha512-n87hHC/miSaUaKdnQygz+2wlj2KI2rsGazDPaKF0wbH8i87um9HPDWyz3VdbkqPN79e0qbVfMR9pY7GZQm/X8w==} + '@better-auth/kysely-adapter@1.7.7': + resolution: {integrity: sha512-9FONOCgOcrQI9wJ5v1oDGIg9sT7pa9RjZZQ4fsCTudo8R87SrIGNUvUxDFTJhZvUblvFfGaIBR9Vb8LA+8bqLQ==} peerDependencies: - '@better-auth/core': ^1.6.29 + '@better-auth/core': ^1.7.7 '@better-auth/utils': 0.4.2 kysely: ^0.28.17 || ^0.29.0 peerDependenciesMeta: kysely: optional: true - '@better-auth/memory-adapter@1.6.29': - resolution: {integrity: sha512-F1yLUbNTc/pikEaTlgWOgYlT2RDSi2dV1ysWxacRmQhYtLGYAun1JgU7TDdcEjts+wrLcm+JzRzt5F+ZgNfz9A==} + '@better-auth/memory-adapter@1.7.7': + resolution: {integrity: sha512-FqKFEe+b5tXXV0gRbyirca+qXgMpLOeJ8Wk19yykb/scQKy0WJ4k22WzEV3TTPctQV9DZhwCXPlDtQhnlOv5Gg==} peerDependencies: - '@better-auth/core': ^1.6.29 + '@better-auth/core': ^1.7.7 '@better-auth/utils': 0.4.2 - '@better-auth/mongo-adapter@1.6.29': - resolution: {integrity: sha512-gyOpfBBCpe2wDnv9CY2T0jirC4oTqk3i5svWYBGkGTAFrKsfJA516NMOs001PiHsEDf/JttZapQv5dkoPD0x2w==} + '@better-auth/mongo-adapter@1.7.7': + resolution: {integrity: sha512-ZnVDuRrXqbf0oHphrEN27oTImNH5NTO26dtWatq5cXyYAsH7goof9QIoTOmxTbBxUQahKBw9T6+9h6n+RtOE+g==} peerDependencies: - '@better-auth/core': ^1.6.29 + '@better-auth/core': ^1.7.7 '@better-auth/utils': 0.4.2 mongodb: ^6.0.0 || ^7.0.0 peerDependenciesMeta: mongodb: optional: true - '@better-auth/prisma-adapter@1.6.29': - resolution: {integrity: sha512-W5lr8AYXwa68qhS7PEpEsisF7ASs9rWJyczoZr8+vYypPBmGMq1Yf8bYJ9ig95zj1wdaQGim2ZSYs8hxZeaKVQ==} + '@better-auth/prisma-adapter@1.7.7': + resolution: {integrity: sha512-4YcnrcVdvWiAZw0sZl63tWUClPXg5r3QsLn7C2sG7kKEBY06zOng0ibecyxRBixKq1zE1ceHuEUqez5Rhi+nQA==} peerDependencies: - '@better-auth/core': ^1.6.29 + '@better-auth/core': ^1.7.7 '@better-auth/utils': 0.4.2 '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 @@ -538,12 +530,12 @@ packages: prisma: optional: true - '@better-auth/telemetry@1.6.29': - resolution: {integrity: sha512-zW1GRIBR/sn83siG+xK4ll/gzfTRxVh6rcpzzjYkR2W9/DkDMwEotl/Mz8OW0Gr2KpuhEkfwiVGH9zp/cyK4Pg==} + '@better-auth/telemetry@1.7.7': + resolution: {integrity: sha512-PaRA6i+kAioVYmza2gHPcdrxP0hAotph4KV9hft2GeNJq+AZXDiBu3FuLM9HxvcmiZeUT74Vufqd9ypmB8veTA==} peerDependencies: - '@better-auth/core': ^1.6.29 + '@better-auth/core': ^1.7.7 '@better-auth/utils': 0.4.2 - '@better-fetch/fetch': 1.3.1 + '@better-fetch/fetch': 1.3.2 '@better-auth/utils@0.4.2': resolution: {integrity: sha512-AUxrvu+HaaODsUyzDxFgwd/8RZ1yZaYo42LXKSrU2oGgR38pS1ij8nqQKNgtTWoYGpNevNXtCfgTy6loHveW9A==} @@ -554,14 +546,23 @@ packages: '@better-fetch/fetch@1.3.1': resolution: {integrity: sha512-ABkD1WhyfPZprKRQI3bhATjeiFuNWC9PXhfGWqL+sg/gKrM977oFrYkdb4msM3hgUGonr7KlOsOFT5TU2rht9g==} + '@better-fetch/fetch@1.3.2': + resolution: {integrity: sha512-Gs7n99b5tqUC6cQAPbV0uED3IraHB6xQbHLQ/C3l7ZFafHScOx9pQ+DYmP5blbLFShVWLqxNUlI9wi4xU/X+ow==} + '@blazediff/core@1.9.1': resolution: {integrity: sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==} '@borewit/text-codec@0.2.2': resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} - '@changesets/apply-release-plan@8.0.0': - resolution: {integrity: sha512-kUd2pbf1w5/AYmBMb0Tt+rkIPCjFJdT0SZMrkOjJT/WV/QbtmvkyB5jkV0oaNPheavprZk+SfUiozUti7TIL2w==} + '@cacheable/memory@2.2.0': + resolution: {integrity: sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==} + + '@cacheable/utils@2.5.0': + resolution: {integrity: sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==} + + '@changesets/apply-release-plan@8.1.0': + resolution: {integrity: sha512-M93HOGyX3ssg6He3b5NotaHtQVu6uajHS6UIQ28xKt2RzskCy73ayX4I914qBKjTJmrE4YFlELQjfcqxbmGLJw==} engines: {node: ^22.11 || ^24 || >=26} '@changesets/assemble-release-plan@7.0.0': @@ -572,8 +573,8 @@ packages: resolution: {integrity: sha512-3Dst2Ime2Op5nd4XmWJLPIgp11ZFqJqSkVug9izK6TDcIV4YlhPS4ECbEVR+eGI0bk0r1ItogD4j2Oli87bJrA==} engines: {node: ^22.11 || ^24 || >=26} - '@changesets/cli@3.0.1': - resolution: {integrity: sha512-3IVpRSgiKDj2aRbBHKtWTXSRH2/iR3bWxau9iQUoEsZjDhRIj9nhl90k7FWMxZAJvLgJBbgMq8+qS0xQsenYsQ==} + '@changesets/cli@3.0.2': + resolution: {integrity: sha512-t/omGJj/I+Jv0kmJAkj5cstYEdUQiJnpip2F+2m3F4lQ3kAZ3o8Exep4/Fm1qq4rvw6ovlhJvZNrKdwLJ4lkuQ==} engines: {node: ^22.11 || ^24 || >=26, npm: '>=10.9.0', pnpm: '>=10.0.0', yarn: '>=4.5.2'} hasBin: true @@ -596,8 +597,8 @@ packages: '@changesets/get-github-info@0.8.0': resolution: {integrity: sha512-cRnC+xdF0JIik7coko3iUP9qbnfi1iJQ3sAa6dE+Tx3+ET8bjFEm63PA4WEohgjYcmsOikPHWzPsMWWiZmntOQ==} - '@changesets/git@4.0.0': - resolution: {integrity: sha512-uIEswpPUgzBBqrC0qg13byNaPorzhf05LE2T+gRizEKCXvMsJC6NPJp5iNDSV/gYj4Viqh09UiOF5E7XWeH5lQ==} + '@changesets/git@4.0.1': + resolution: {integrity: sha512-6vWpIAC4LkpmlqaIu37ViT5enyd9+uBwAiGqCGhASvkSHBgx4PrtTGXWTMFYpDmZbjgyonyVgMciPrW4MqtJsA==} engines: {node: ^22.11 || ^24 || >=26} '@changesets/parse@1.0.0': @@ -608,8 +609,8 @@ packages: resolution: {integrity: sha512-Zm/6YliV/a2oeWTqHJf6KxLrQwgcK1i/BRDl2m0EKZvbnxV5fG9QRhwJJGshjsZTUTS6dkfURQ2K6aAvgNw/3Q==} engines: {node: ^22.11 || ^24 || >=26} - '@changesets/read@1.0.0': - resolution: {integrity: sha512-8TdE2PwG6yArPt5Ozej83Z6iHz1G8BDBKvIEKZ455MccR4K3GNbLR2XqjBxa+5yLFnEd3xAOPXYboBg1pt8stQ==} + '@changesets/read@1.0.1': + resolution: {integrity: sha512-nzvSC6RcTiWf/dsuwZFXpLzGGsRHYCL68U3uHV7srsulU93aVWY7u2GEJiDZ+4GIIElfaW5EqtKC0UHroY+LTQ==} engines: {node: ^22.11 || ^24 || >=26} '@changesets/should-skip-package@1.0.0': @@ -627,17 +628,20 @@ packages: resolution: {integrity: sha512-q/ThtP9gcnEP6xlv7LrY26C2mHqdGBti/MmOVngt/M/oIGYkssmQGxPK9WzBNt2juVcH/vml2WQ+ra8LXYOTaA==} engines: {node: ^22.11 || ^24 || >=26} - '@chevrotain/cst-dts-gen@10.5.0': - resolution: {integrity: sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==} + '@chevrotain/cst-dts-gen@12.0.0': + resolution: {integrity: sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg==} + + '@chevrotain/gast@12.0.0': + resolution: {integrity: sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ==} - '@chevrotain/gast@10.5.0': - resolution: {integrity: sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==} + '@chevrotain/regexp-to-ast@12.0.0': + resolution: {integrity: sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA==} - '@chevrotain/types@10.5.0': - resolution: {integrity: sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==} + '@chevrotain/types@12.0.0': + resolution: {integrity: sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA==} - '@chevrotain/utils@10.5.0': - resolution: {integrity: sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==} + '@chevrotain/utils@12.0.0': + resolution: {integrity: sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==} '@clack/core@1.4.3': resolution: {integrity: sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==} @@ -733,8 +737,8 @@ packages: conventional-commits-parser: optional: true - '@conventional-changelog/template@1.3.0': - resolution: {integrity: sha512-GsCw/qu92GI0EX6s7fxUi/SG1lmFjG9XZivxxEDZXqztuQKCn5o5wKdz4v005zci0Md7EZzgAwmQLoIEDZoaow==} + '@conventional-changelog/template@1.4.0': + resolution: {integrity: sha512-aalGyl7dbB5PArRebDIX43ZvBlXrYm9uWzGJ26t+4SzJVPsOuvfILGGbw5X4yX7i50YEmJ8zvbiWnqH/AAnZqg==} engines: {node: '>=22'} '@date-fns/tz@1.2.0': @@ -800,6 +804,9 @@ packages: '@emnapi/runtime@1.11.1': resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + '@emoji-mart/data@1.2.1': + resolution: {integrity: sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==} + '@emotion/babel-plugin@11.13.5': resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} @@ -1328,8 +1335,8 @@ packages: resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@eslint/plugin-kit@0.7.2': - resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} + '@eslint/plugin-kit@0.7.3': + resolution: {integrity: sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@exodus/schemasafe@1.3.0': @@ -1565,8 +1572,8 @@ packages: cpu: [x64] os: [win32] - '@internationalized/date@3.12.3': - resolution: {integrity: sha512-fuLX+3ZKLsxI73y8b01EG/WjHb6gE6weCqlfawPO27kBWGMh9G1yH6Csv1uU7/cac9H2GHmOMt6CjmuQ1aia4Q==} + '@internationalized/date@3.12.4': + resolution: {integrity: sha512-M1dEn4c1U1HsSlaVR8upZtSqvXrTkHDfv18H01uCSJyjVLDxnBR38v/fMxecmlwXKR4i9HeZcmgQAPE6A+aGJQ==} '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -1581,12 +1588,24 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} '@jsdevtools/ono@7.1.3': resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} + '@keyv/bigmap@1.3.1': + resolution: {integrity: sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==} + engines: {node: '>= 18'} + peerDependencies: + keyv: ^5.6.0 + + '@keyv/serialize@1.1.1': + resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} + '@lexical/clipboard@0.41.0': resolution: {integrity: sha512-Ex5lPkb4NBBX1DCPzOAIeHBJFH1bJcmATjREaqpnTfxCbuOeQkt44wchezUA0oDl+iAxNZ3+pLLWiUju9icoSA==} @@ -1664,8 +1683,8 @@ packages: peerDependencies: yjs: '>=13.5.22' - '@lucide/svelte@1.33.0': - resolution: {integrity: sha512-b+osTYG2V4dge5Lr7tnaTaahhy/4vH7Xb8zcqVjmctjwgkpXS0NNOJPkGzHHZoxtw/OO+2YAU28omeMfYCaawg==} + '@lucide/svelte@1.54.0': + resolution: {integrity: sha512-PbUrcmVPU71zmcs1Kc5RlgF83SsBsSNC+MeuttAHMmvPMuoIJfyDPjit5kpAJxJwsmN2Pk1Bx75AwZxP8vkDvw==} peerDependencies: svelte: ^5 @@ -1691,9 +1710,9 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@mrleebo/prisma-ast@0.13.1': - resolution: {integrity: sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==} - engines: {node: '>=16'} + '@mrleebo/prisma-ast@0.16.0': + resolution: {integrity: sha512-a9ELYNIflEQP38tSu6gnUgSAWgXjuhMvC52868K5sWgyRmYsjiJMWTUmm8iy7hZT5EN2ZKVydMTFP2q3/+6ccg==} + engines: {node: '>=22.0.0'} '@napi-rs/lzma-linux-x64-gnu@1.5.1': resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} @@ -1760,12 +1779,12 @@ packages: cpu: [x64] os: [win32] - '@noble/ciphers@2.3.0': - resolution: {integrity: sha512-Clu/xdfgVTf9o7ngLOURaxePwR0j8sjclKEtVij10/jGulwFsPWCvvRgG/XjUVf8Nei+jLG6uwyXzUTGY1DQrw==} + '@noble/ciphers@2.4.0': + resolution: {integrity: sha512-AnjFn0Jv92laAkvMrghlFZq4qQCIN/4DxFV/eooqtC2YTjB7kBeLMS2T9KJX4Dn+ZVXLOwK0lSgqDtx9gvxtiw==} engines: {node: '>= 20.19.0'} - '@noble/hashes@2.3.0': - resolution: {integrity: sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==} + '@noble/hashes@2.4.0': + resolution: {integrity: sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==} engines: {node: '>= 20.19.0'} '@opentelemetry/api-logs@0.220.0': @@ -1810,223 +1829,268 @@ packages: resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} engines: {node: '>=14'} - '@orval/angular@8.24.0': - resolution: {integrity: sha512-tmDca47QJdB7+suSUdjki3oURBP/EHhHkvgbOmm4KPy2G0YBKHSEm/oMphE5fab4TBzMNEVoiBi5PnK7m6Hu3g==} + '@orval/angular@8.31.0': + resolution: {integrity: sha512-8nhsbPnXBztqVvuSDID17jc3OEd+P1Yhv9iwCwGR4ZBafU4kqaqQOq5v1lHzAN20DS0srrtn48d/TTF6Pk+rMA==} - '@orval/axios@8.24.0': - resolution: {integrity: sha512-cldt9cGeZrgNtUb4iLeyE8mLh1PJisfGhdw4jmR+d1ZQl6H2ZjS7mQ+e+KU1SbZ1dITHeuZUC9I9UwHtZ882gw==} + '@orval/axios@8.31.0': + resolution: {integrity: sha512-jc46yYqraSIGqvlhsRftglY2nDH/BeRUYmNi32DpifSS3t4qq+gPWrOIQDV8GTTio/kNc5j1oeRX7YClyH/OVA==} - '@orval/core@8.24.0': - resolution: {integrity: sha512-2aHwQ+3wFNu4rjqMlITSqtsO8YLvvyk+hvBdyLmrKTdyk3De9gQBoMynnLwVU0V6MXgb2a7x7bd1nnRelGJHJA==} + '@orval/core@8.31.0': + resolution: {integrity: sha512-Onqvva4LcQZqFAPobyyhFHl3Ml4V5HkCaDFDvyMlwEmudFS7PVx91r1YeJFV9Sv0OZd0lZ8YtsUvrtXrP9JrvQ==} peerDependencies: - '@faker-js/faker': '>=10' + '@faker-js/faker': '>=8' peerDependenciesMeta: '@faker-js/faker': optional: true - '@orval/effect@8.24.0': - resolution: {integrity: sha512-+0pj5Fy6mHpUva9AwIddfgNSQWfZKbt+4AsJVCU6YuhE7GBme0LppTpB6FCYl3GarbwE2PY8ApTQ8Wgt8hvO8Q==} + '@orval/effect@8.31.0': + resolution: {integrity: sha512-4zNw/TE7coJqmrkH96TFirlvzXnpKqiOtvX9ggj+oKGNgvNI8C3jc7EhjGBdKwg94vIOFFm9MjwYKySHFZV6AA==} + peerDependencies: + effect: '>=3' + peerDependenciesMeta: + effect: + optional: true - '@orval/fetch@8.24.0': - resolution: {integrity: sha512-Wfw4rkeTHhTmGyJYMCc3ar/rczymkskxN1g4+koVmmynhIziwlHJAaNXc4uwnbEhg6S/T0QGrX9OTGSmd+NDSg==} + '@orval/fetch@8.31.0': + resolution: {integrity: sha512-4s+tZwa6mIIEG+dU7BGIKs+1JlhixpvlZAKkW+0xIL+xd1dzPeBKQLKdi9Z+9bZgJaLDkHeS5oH0I+YMdTcYAA==} - '@orval/hono@8.24.0': - resolution: {integrity: sha512-CTEg31oZwj1ZkFVaqg6V8fbc5qz9f72ScYQvk6EOGRbV7Q/hDhYDuWbpscw16epW9KcUlo3N3Zf20haMTLz4yw==} + '@orval/hono@8.31.0': + resolution: {integrity: sha512-Tmu1NKpi3gKDXshb7u/eowwyXHdK4LF0/lhdyRcIiWyunqW8BF3XVQ9CpWaqzwNjEXlhsudbCpG7CGtkxsvbQg==} peerDependencies: typescript: '>=5' peerDependenciesMeta: typescript: optional: true - '@orval/mcp@8.24.0': - resolution: {integrity: sha512-nbO2sYmtdMkeBsl74BfpoaYYNrelbH+/0ss9xWXX6sW7IZoJK8NqhiaJ4idEuyQ/fF4g7fTBmsWipZdEUDLYdQ==} + '@orval/mcp@8.31.0': + resolution: {integrity: sha512-bel8hsDNVGueFiKWScGu5PoS09kOD0GfLCP4rhYw0pMbr5rZP6N3/OtQw5ErIe4JZZH3Yu3qCCbbxOVJDCrgBw==} + peerDependencies: + '@modelcontextprotocol/sdk': '>=1' + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true - '@orval/mock@8.24.0': - resolution: {integrity: sha512-XjnZM/0WQdlh/+IGT3P/UjKvYzOm+89avkd5wadC0StBpHgGCnYWuENmoLdkbOBhAeyeL6l9Jp11+mOllU6MZw==} + '@orval/mock@8.31.0': + resolution: {integrity: sha512-z8q03Pb3riemJGaxqih8w3LkfB4wXReWEEqQuvVLdsHWc+9fZLaKkFegKQ8tAwvrvfpPbZbhe2taqOsAvGfymw==} + peerDependencies: + '@faker-js/faker': '>=8' + msw: '>=2' + peerDependenciesMeta: + '@faker-js/faker': + optional: true + msw: + optional: true - '@orval/query@8.24.0': - resolution: {integrity: sha512-E3biV70vHOUKJSn+no6nJAd6Pg40V4PnyHXtR/U7lAaPTAQPM7k9tKpRaGoIijogmmX6h06yOI687HU1N/RSJA==} + '@orval/query@8.31.0': + resolution: {integrity: sha512-Rym6C4MX07/a7kYyhdVoPGeSdetRuSie+uAAIcszdf28IDpd12ICzOdvuZ/sEcSWvqhv14vfIxQTuUMbPFkFgw==} + peerDependencies: + '@tanstack/angular-query-experimental': '>=5' + '@tanstack/react-query': '>=4' + '@tanstack/solid-query': '>=4' + '@tanstack/svelte-query': '>=4' + '@tanstack/vue-query': '>=4' + peerDependenciesMeta: + '@tanstack/angular-query-experimental': + optional: true + '@tanstack/react-query': + optional: true + '@tanstack/solid-query': + optional: true + '@tanstack/svelte-query': + optional: true + '@tanstack/vue-query': + optional: true - '@orval/solid-start@8.24.0': - resolution: {integrity: sha512-X+T7GaHbe1GgR7ltpiFiw4F6klj2pGyCNqkHxTsc4SA/GwiFemBAyBXSdIhMTPU4JObehiFB4soojmS/WPnzBw==} + '@orval/solid-start@8.31.0': + resolution: {integrity: sha512-qKcqs1cpF4DHifnvMW6GsYpr7NPOKotXoMM9jhYeIeun7hLbm0XxuNJrxFlO74quHCcIrXOxj9+ly68q/3YXDw==} - '@orval/swr@8.24.0': - resolution: {integrity: sha512-7YBfqSUGpbZRm6siBm1vYa4qz7ukm+uimDfW6M1tOfCyySfve3SIMPMI0c/Sl7hmWdBZYcSYqRyasG7Nr9qBtg==} + '@orval/swr@8.31.0': + resolution: {integrity: sha512-U4vzAqVpXwgR4un2jbyDqjSqyx+Zi4Rd37EY62nF7VkGo9o9W+8YKg03xRrSITURNEdKhIqMo32B6Fxi/rJweA==} + peerDependencies: + swr: '>=2' + peerDependenciesMeta: + swr: + optional: true - '@orval/zod@8.24.0': - resolution: {integrity: sha512-TXK+tQbh9y/q2XlneOqmwg3B6BsbjnLJhkG1/1dMnK3EDdqbf5HSmYKndejJmloomgAdugv0OGyOgmBdSaeisg==} + '@orval/zod@8.31.0': + resolution: {integrity: sha512-Rm9WsHDHRHg6e2pF80iGPyeERiI2G/5dzwZHIDFzTadYhkhUzW2YBzEm5GfW8fNR6unD38QaMrO1Xfjv2DahQQ==} + peerDependencies: + zod: '>=3' + peerDependenciesMeta: + zod: + optional: true '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} - '@oxc-project/types@0.146.0': - resolution: {integrity: sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==} + '@oxc-project/types@0.147.0': + resolution: {integrity: sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==} - '@oxfmt/binding-android-arm-eabi@0.64.0': - resolution: {integrity: sha512-o6uzh/jTOQeAY5TdkAeXdqv7MBRcPxiRA08zrcBtkKj5cSu/FMu0Hl7Q6Fi1KCKyCWZ6lJVjBzdsJvsKltUsGQ==} + '@oxfmt/binding-android-arm-eabi@0.67.0': + resolution: {integrity: sha512-2olh3ioEmc4gRzQm7jxyB1b/PFBoFvTq8KdgYySeNpysDtA6DEg2Mvya4/I6flhL7G0eOrE8RD7JCNCIMhE16Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxfmt/binding-android-arm64@0.64.0': - resolution: {integrity: sha512-jRGSUeeP7p3Gynw2YaCVtjBIA6ZxY6bEB/ES5i54OhqmRTyuVg7ZgstEtzgq6GOAJd+2QZ5pvf+bFfmW5Mp9cw==} + '@oxfmt/binding-android-arm64@0.67.0': + resolution: {integrity: sha512-ulfw8EHN1MBq/MFFDXw2/M1VAFu5mRUcnuZ8Hqbv9viAnFzO9t1jKSAsDqKYYDGMlytF/uj6Z5z5n/tHupnKhw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxfmt/binding-darwin-arm64@0.64.0': - resolution: {integrity: sha512-JINwtU2lW7nOFSqi+H2qplipNUqah9Gc1jgGmB82kTD4UnZrZIVxCJ9qEmFiKfjNq27gYLFhrUb0to86aCwMjw==} + '@oxfmt/binding-darwin-arm64@0.67.0': + resolution: {integrity: sha512-MfONZx/O2o9M5v2jDFol556G9+A+P9xCuJ4DZ+qhE+RnaCdoscy6Eu5nq1dbuNxhwdJyZ6kLI7fnG9mwEeOeGg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxfmt/binding-darwin-x64@0.64.0': - resolution: {integrity: sha512-gCmuswrgrOSajV4HCRFkVCGIruPq8bjYuPYgSE2WQB3mD6XrdyZ3JMSRZCkQ8zCxOyGWriBo6QoZ5nmMHQ1BfA==} + '@oxfmt/binding-darwin-x64@0.67.0': + resolution: {integrity: sha512-CYnIx5LvFVJnyJcCqwH2jxMKjFjqo5678MPjdmNFoSGMhlOvZ/xRZqvhDcolKrXc8fezW3AKh+C4wyoFuWOSSg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxfmt/binding-freebsd-x64@0.64.0': - resolution: {integrity: sha512-Ab8g7a38pT0MMImjh7anRSTve6buWBIlcXIFBYa5xl4s6UxEgKSc2xOOhbGtLwvXnEi2PsEDGoJh3oUU7xkehQ==} + '@oxfmt/binding-freebsd-x64@0.67.0': + resolution: {integrity: sha512-7/iF1orvIS9mxhKUqnmtMgm+OrSQ5acPwuvdQrm6ECgqbwPmC+Pw9cdke3sNfVN6pT2hbJ58+jP8BCThl5HXOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxfmt/binding-linux-arm-gnueabihf@0.64.0': - resolution: {integrity: sha512-BgvS3CoQ+Xy2deoZqEN8JVKabcCZi2RxA3yant8G9OAv9KuPJ9TCjHkqigzdHUVwErZxEP5d2bzLIEyKYyBDLg==} + '@oxfmt/binding-linux-arm-gnueabihf@0.67.0': + resolution: {integrity: sha512-yy+OGys07IZOpOmYPZoObKyUQLkfxeQqeCypk+1jaZd8HGo77hzvU1Jg8X3+W75o+9lszOjBfg0nkGtlwYywXw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm-musleabihf@0.64.0': - resolution: {integrity: sha512-QXpNxwoMj0YvnceCNZadNSden3bIcnvjn/sDp/rwZhRoZoZYGpHvtPyhGsdJz9uvT9GkaMW7SsLddurU56dt8w==} + '@oxfmt/binding-linux-arm-musleabihf@0.67.0': + resolution: {integrity: sha512-wPIeeigXgJpwNw3wydYRt3U9iN9Y/ejpOZuYL9IA7igxWs7LIQMOkhKxTumRvy6dIv0iXKk3RTw3Vmjg0i+2sg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm64-gnu@0.64.0': - resolution: {integrity: sha512-BBgH3I1ppDsI5pZ4Pdhw0ceYxwVCfbU/bZEBCeZ6caRS9x0ZabErxubP7riGUn11PXZBhe8DYdjkDKP1FlVQ5w==} + '@oxfmt/binding-linux-arm64-gnu@0.67.0': + resolution: {integrity: sha512-0+XNxcdbkTfxdcD4qW6Ci9n+mBNJ8xTBumnxKvKBmRFOdx0Wf8/KiHjCJayooXmYkqRpRVd98Q5egvzx5BLSgQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-arm64-musl@0.64.0': - resolution: {integrity: sha512-v19HSjC/BGXdt26qEvKZtwAHgGmQ2Agcap2kQP+KIqoRZqivVzYth3ui2dJA1i+6/fjpjga85lIOaJJjQ/bOOw==} + '@oxfmt/binding-linux-arm64-musl@0.67.0': + resolution: {integrity: sha512-I75LKPJyNOYUzkqAiAMIE31+Ye7xtQXZdoty1IXn4B+bw5Zpmez5wfG19ejGpNnS/BzQ7LFS+7jxuTPb+vHiZw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-ppc64-gnu@0.64.0': - resolution: {integrity: sha512-PElLnOo4xFTBZrxPhgTIj0eHqZXwEBQoNWtb7facUV170T0B0FRET0iNbb3LUeLWTybkUW+vsdyv4ihOdyXGyw==} + '@oxfmt/binding-linux-ppc64-gnu@0.67.0': + resolution: {integrity: sha512-c2M5iRpe1QMZSRE/UvZoPdXBWb5Ic/ycvOyNiKCqPwQ/OyOKIMiJs02ynlNnjb7ZZJnRXYLmGcohoINOcwDK3w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-gnu@0.64.0': - resolution: {integrity: sha512-Qzsg15n4F5CH+MorcRW4MkAEMiLzXmeG+DiDSbP/bBTqCmWOH3K9DHryNrve+JHlV0txS+B6Z9P5Xz+cmWeL+g==} + '@oxfmt/binding-linux-riscv64-gnu@0.67.0': + resolution: {integrity: sha512-dQzzYlV24Udhfm5ECuSdgqRvFJU/CGHzcYYEO3dLM6W6+CHiBFrq9OjIllkdCcPhsoSQ8o223Dja84MOSzed9A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-musl@0.64.0': - resolution: {integrity: sha512-/GZ358wnQ/Ez4UVnCcZIi56JkY0sOdZ+B108pqXKqZz3jLS59F4KEAB1Qv3fRlObrFEk+3L2vUQ/xoPx+3vjXw==} + '@oxfmt/binding-linux-riscv64-musl@0.67.0': + resolution: {integrity: sha512-rFNq1CgX4qMJANOq42LkAs90JE80GpiaEohAV2qn/gT2hGjQTW1zBO5zQBxArI4926pM1OSzo3CN0tBszGBIaA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-s390x-gnu@0.64.0': - resolution: {integrity: sha512-/C9We3DXegowfLXtVCYHeNiU9azwCDr5cQkEtCVlc74vyn+lLQSPApJ1CZmxAduqeq/Oi3gQ+IVptyhCaTMtkQ==} + '@oxfmt/binding-linux-s390x-gnu@0.67.0': + resolution: {integrity: sha512-Sky6rEdz2o5IGq01lPhS12yEvDdChVEcaYrcLHkveh4Fx0qPjljE/Iul6SX/bRMl6lNc8J7J/mDQdzgBdA++Pg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-gnu@0.64.0': - resolution: {integrity: sha512-91KM2CeRWscIEHlj1NsW2WSnzGeq1Ehq+39bfDowTdkn+fcvK/x4Y1RcyqT7glyBjZio0ldkeCG6Usj3v7ASog==} + '@oxfmt/binding-linux-x64-gnu@0.67.0': + resolution: {integrity: sha512-vPXmlNORV8AZq2Ocxh07pxwMjfENUWCV/eZArnao0qC3NO/hDeTVkQvee7SJJUbIiF5PZbBa4kYmaXnu7Rk58w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-musl@0.64.0': - resolution: {integrity: sha512-gw7uEk9I+7zoT1EYLra1eWArIzNcz8e3jkv+Noo2+o2T7wPvsNSQbfoa4DSfZlvn1i6mJ05RiZ4/omaXPDNhQg==} + '@oxfmt/binding-linux-x64-musl@0.67.0': + resolution: {integrity: sha512-x/WAtFqYtVr3vZ9ni8nr4kn9whSitg8fOljq/pZzBpxopRdY1BMLZCZkrbIbaBcYkm46qGbqVea2FCWmtQ2P9w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxfmt/binding-openharmony-arm64@0.64.0': - resolution: {integrity: sha512-HYHFf616FHSPSO07c09mjmXBfQ73wIVM3m0txOiooa5XZkGoxFd6B14PVj0LB0DXIqJ6wAO/dDR/NX/5UUaqnw==} + '@oxfmt/binding-openharmony-arm64@0.67.0': + resolution: {integrity: sha512-eRw9Neh4/aA6i+q/R3WU1gGQINhVM0J4fXIm6t27caOamkr/37uAkp1IdBx4zlJH97hmXR63z/q9n5c5dN7MzA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxfmt/binding-win32-arm64-msvc@0.64.0': - resolution: {integrity: sha512-uQjFp081IZSWD6VAofX2iO2z01awAdHmfC+NrieWIPKrT2hZKQDyq/U18M7ifC0sm0Wz8aHY/p6+FDYIzs/CrQ==} + '@oxfmt/binding-win32-arm64-msvc@0.67.0': + resolution: {integrity: sha512-YIMvb+sGNYN2uc6+QK2HLPeEKM2vl7QZ5onQzpAJRb6pnf0DwUFP5R8tdS9R0l8hdUil2gu4Uxd0Yxrop0iT4w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxfmt/binding-win32-ia32-msvc@0.64.0': - resolution: {integrity: sha512-lNM6byTAQ881jugzFu8juJTbNRgsUTlswMA6pJmwi1XDvmIqnnb49lcUAs5gz94fCJLrVN+/X3s3jOKqx23WIQ==} + '@oxfmt/binding-win32-ia32-msvc@0.67.0': + resolution: {integrity: sha512-LzmU9MyACPzwNDIK0ItMedHPz735Ug7ELWguxo4/kuy6zWuDoeglOAEFCY8jLg0PzRpFO3hDyLFe2Gu2eFDeGA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxfmt/binding-win32-x64-msvc@0.64.0': - resolution: {integrity: sha512-BtmbtL/QjMtF1a6C3CqoDluH2IfB6fJt62E+B9RFfUPtFk4Iz9PFS6+y/SzzOvSxc7aUk2Kphwg7Dh8lMbwu6g==} + '@oxfmt/binding-win32-x64-msvc@0.67.0': + resolution: {integrity: sha512-sbQOIDNLUEeVZcAJcSL5VURn7kfjvilPviody4Yl5n8lQCDtUm+C9oHTTwZS/m4d/Z6Vv3jNEiAofH932NPPCg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@payloadcms/graphql@3.88.0': - resolution: {integrity: sha512-ync+bPFl0UacwBs1wVQkPlaSWWQJc4LjGG9OuRHEW7wYW3aeVQ+H2zfdFL6/cVJGP3zOq+XIlvYYw8G86Ki+Vw==} + '@payloadcms/graphql@3.89.0': + resolution: {integrity: sha512-oa77vdW2yKwla8hXKDQOYvCLZNn6rmR8Th2QgGfq3gAvlR5hEnZ7KH9VK23AfkimXRuHKnI0tH8K+OaBOKdStA==} hasBin: true peerDependencies: graphql: ^16.8.1 - payload: 3.88.0 + payload: 3.89.0 - '@payloadcms/next@3.88.0': - resolution: {integrity: sha512-3f2YOGSmtTp85YnlJm9FusRgshKJbiLI8uXs8iQNMwU7RZJnLCURG8iv/6sjKHO0owIfw3vIL5qgxdtIBuQwpA==} + '@payloadcms/next@3.89.0': + resolution: {integrity: sha512-1PEzFEQIRhrOQh1cKegmNcaOQxWXA5wflxMkgqZqKZN62i64/gMB4MWspyfBEHjiAT0NPFDZyK7e522qEOj4vg==} engines: {node: ^18.20.2 || >=20.9.0} peerDependencies: graphql: ^16.8.1 next: '>=15.2.9 <15.3.0 || >=15.3.9 <15.4.0 || >=15.4.11 <15.5.0 || >=16.2.6 <17.0.0' - payload: 3.88.0 + payload: 3.89.0 - '@payloadcms/richtext-lexical@3.88.0': - resolution: {integrity: sha512-lY8oIUiggykWKB3ljMfSidxBtA8ahMq0Qqp28OuvaCNIDp0HTMIPxcHvFOQZngMqtQHhpautD6vcQR0Q9CEHHQ==} + '@payloadcms/richtext-lexical@3.89.0': + resolution: {integrity: sha512-IV9sKT/fYptRJXk1XfIKW6V2sBWma4hofBMPI3b1Ab0ClKlyudwrtRtqv9cZzxwoNMYhtC8h+3SQMOq10eI2bQ==} engines: {node: ^18.20.2 || >=20.9.0} peerDependencies: '@faceless-ui/modal': 3.0.0 '@faceless-ui/scroll-info': 2.0.0 - '@payloadcms/next': 3.88.0 - payload: 3.88.0 + '@payloadcms/next': 3.89.0 + payload: 3.89.0 react: ^19.0.1 || ^19.1.2 || ^19.2.1 react-dom: ^19.0.1 || ^19.1.2 || ^19.2.1 - '@payloadcms/translations@3.88.0': - resolution: {integrity: sha512-AZVQjjW5pKxO+K9ODeh6yw6/EfXK1hxGPw55KzzDC5srO3hjNskgyWtkauKvbz+S0+xFspANbRc4OD/Rg/74WA==} + '@payloadcms/translations@3.89.0': + resolution: {integrity: sha512-XLPOdv9NoPcqr21zBTu0uyJk0qGRIF7JVl2H2UTecpweQSMlQCackhUKWz1LKBO7QcKgkNSVHFnMNcizRDcPDg==} - '@payloadcms/ui@3.88.0': - resolution: {integrity: sha512-iwQhHPDn4Oj2bPg1UV467BoxfUNo1rUmD8jMZaw2Vtt/nmxAPl3W3CWN/CsOX/yITuPuGHJxznOg4PZNMF3gEg==} + '@payloadcms/ui@3.89.0': + resolution: {integrity: sha512-jKc8kgSjaICZ39sNC4dr4MLkAj2lR9ikzSwcHzPGOsJlleOVPJq5jM9bmAnqEOhtbpPNl0CGlpo6RIeECnBmwg==} engines: {node: ^18.20.2 || >=20.9.0} peerDependencies: next: '>=15.2.9 <15.3.0 || >=15.3.9 <15.4.0 || >=15.4.11 <15.5.0 || >=16.2.6 <17.0.0' - payload: 3.88.0 + payload: 3.89.0 react: ^19.0.1 || ^19.1.2 || ^19.2.1 react-dom: ^19.0.1 || ^19.1.2 || ^19.2.1 '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} - '@playwright/test@1.62.1': - resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==} + '@playwright/test@1.63.0': + resolution: {integrity: sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==} engines: {node: '>=20'} hasBin: true @@ -2043,98 +2107,98 @@ packages: '@preact/signals-core@1.14.4': resolution: {integrity: sha512-HNB6HYeYKhQbJ1aKl+YRjrS4+QWHLKX6qKoUsfS/m0vqzsVaEBiZiaKbG/e+NKk2ch5ALQr/ihWaMHxiCuuWHA==} - '@rolldown/binding-android-arm-eabi@1.2.5': - resolution: {integrity: sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==} + '@rolldown/binding-android-arm-eabi@1.2.6': + resolution: {integrity: sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@rolldown/binding-android-arm64@1.2.5': - resolution: {integrity: sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==} + '@rolldown/binding-android-arm64@1.2.6': + resolution: {integrity: sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.2.5': - resolution: {integrity: sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==} + '@rolldown/binding-darwin-arm64@1.2.6': + resolution: {integrity: sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.2.5': - resolution: {integrity: sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==} + '@rolldown/binding-darwin-x64@1.2.6': + resolution: {integrity: sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.2.5': - resolution: {integrity: sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==} + '@rolldown/binding-freebsd-x64@1.2.6': + resolution: {integrity: sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.2.5': - resolution: {integrity: sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.6': + resolution: {integrity: sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.2.5': - resolution: {integrity: sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==} + '@rolldown/binding-linux-arm64-gnu@1.2.6': + resolution: {integrity: sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.2.5': - resolution: {integrity: sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==} + '@rolldown/binding-linux-arm64-musl@1.2.6': + resolution: {integrity: sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.2.5': - resolution: {integrity: sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==} + '@rolldown/binding-linux-ppc64-gnu@1.2.6': + resolution: {integrity: sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.2.5': - resolution: {integrity: sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==} + '@rolldown/binding-linux-s390x-gnu@1.2.6': + resolution: {integrity: sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.2.5': - resolution: {integrity: sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==} + '@rolldown/binding-linux-x64-gnu@1.2.6': + resolution: {integrity: sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.2.5': - resolution: {integrity: sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==} + '@rolldown/binding-linux-x64-musl@1.2.6': + resolution: {integrity: sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.2.5': - resolution: {integrity: sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==} + '@rolldown/binding-openharmony-arm64@1.2.6': + resolution: {integrity: sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-win32-arm64-msvc@1.2.5': - resolution: {integrity: sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==} + '@rolldown/binding-win32-arm64-msvc@1.2.6': + resolution: {integrity: sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.2.5': - resolution: {integrity: sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==} + '@rolldown/binding-win32-x64-msvc@1.2.6': + resolution: {integrity: sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -2187,141 +2251,141 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.62.4': - resolution: {integrity: sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==} + '@rollup/rollup-android-arm-eabi@4.63.1': + resolution: {integrity: sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.62.4': - resolution: {integrity: sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==} + '@rollup/rollup-android-arm64@4.63.1': + resolution: {integrity: sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.62.4': - resolution: {integrity: sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==} + '@rollup/rollup-darwin-arm64@4.63.1': + resolution: {integrity: sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.62.4': - resolution: {integrity: sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==} + '@rollup/rollup-darwin-x64@4.63.1': + resolution: {integrity: sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.62.4': - resolution: {integrity: sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==} + '@rollup/rollup-freebsd-arm64@4.63.1': + resolution: {integrity: sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.62.4': - resolution: {integrity: sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==} + '@rollup/rollup-freebsd-x64@4.63.1': + resolution: {integrity: sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.62.4': - resolution: {integrity: sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==} + '@rollup/rollup-linux-arm-gnueabihf@4.63.1': + resolution: {integrity: sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==} cpu: [arm] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.62.4': - resolution: {integrity: sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==} + '@rollup/rollup-linux-arm-musleabihf@4.63.1': + resolution: {integrity: sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==} cpu: [arm] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.62.4': - resolution: {integrity: sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==} + '@rollup/rollup-linux-arm64-gnu@4.63.1': + resolution: {integrity: sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.62.4': - resolution: {integrity: sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==} + '@rollup/rollup-linux-arm64-musl@4.63.1': + resolution: {integrity: sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.62.4': - resolution: {integrity: sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==} + '@rollup/rollup-linux-loong64-gnu@4.63.1': + resolution: {integrity: sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==} cpu: [loong64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-loong64-musl@4.62.4': - resolution: {integrity: sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==} + '@rollup/rollup-linux-loong64-musl@4.63.1': + resolution: {integrity: sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==} cpu: [loong64] os: [linux] libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.62.4': - resolution: {integrity: sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==} + '@rollup/rollup-linux-ppc64-gnu@4.63.1': + resolution: {integrity: sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.62.4': - resolution: {integrity: sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==} + '@rollup/rollup-linux-ppc64-musl@4.63.1': + resolution: {integrity: sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==} cpu: [ppc64] os: [linux] libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.62.4': - resolution: {integrity: sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==} + '@rollup/rollup-linux-riscv64-gnu@4.63.1': + resolution: {integrity: sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.62.4': - resolution: {integrity: sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==} + '@rollup/rollup-linux-riscv64-musl@4.63.1': + resolution: {integrity: sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==} cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.62.4': - resolution: {integrity: sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==} + '@rollup/rollup-linux-s390x-gnu@4.63.1': + resolution: {integrity: sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.62.4': - resolution: {integrity: sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==} + '@rollup/rollup-linux-x64-gnu@4.63.1': + resolution: {integrity: sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.62.4': - resolution: {integrity: sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==} + '@rollup/rollup-linux-x64-musl@4.63.1': + resolution: {integrity: sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.62.4': - resolution: {integrity: sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==} + '@rollup/rollup-openbsd-x64@4.63.1': + resolution: {integrity: sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.62.4': - resolution: {integrity: sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==} + '@rollup/rollup-openharmony-arm64@4.63.1': + resolution: {integrity: sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.62.4': - resolution: {integrity: sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==} + '@rollup/rollup-win32-arm64-msvc@4.63.1': + resolution: {integrity: sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.62.4': - resolution: {integrity: sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==} + '@rollup/rollup-win32-ia32-msvc@4.63.1': + resolution: {integrity: sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.62.4': - resolution: {integrity: sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==} + '@rollup/rollup-win32-x64-gnu@4.63.1': + resolution: {integrity: sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.62.4': - resolution: {integrity: sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==} + '@rollup/rollup-win32-x64-msvc@4.63.1': + resolution: {integrity: sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==} cpu: [x64] os: [win32] @@ -2329,24 +2393,32 @@ packages: resolution: {integrity: sha512-IAfnpIZnXY6ni+zyFMwWHBa5b/7yr4mCSvoTGR84oS9q4Quy2wjgafnr7CyfL65ney3iilBZHigZYLYqdqCu3Q==} engines: {node: '>=22'} + '@scalar/helpers@0.11.1': + resolution: {integrity: sha512-Knwbe0IYqFk0PPDoOKLasqglBHfyf9/zwWWqFsSNi/AtdjM29wSZXN6p8DFid6iB5B9epYH9YiSgJ6tpD00TEw==} + engines: {node: '>=22'} + '@scalar/json-magic@0.12.20': resolution: {integrity: sha512-5JawJ1L3+ddDoKefkXsklKs6CmmTIcPDmkJPpAkbRi9QQiU9MUONQ4VSi2z3ij+8Ka4neNhsXBNG4itPjurBLg==} engines: {node: '>=22'} - '@scalar/json-magic@0.13.0': - resolution: {integrity: sha512-K25TprVh4fusvKJlh4qnqh+cb4rpixv2xPTC2bqanhO9DXHQIesq7pnE0XONsfQJn3y9VjDefICIXHEXFqY3Pw==} + '@scalar/json-magic@0.13.2': + resolution: {integrity: sha512-T8rQw5u7+MSTDpUcd5ShX1taOUxpZMv2b/P6xsahdlv/u68VX/Bq/+uzuAf2xW8IIOy7BEP4MBggle/vMDgAXw==} engines: {node: '>=22'} - '@scalar/openapi-parser@0.28.14': - resolution: {integrity: sha512-OECzu3Iu8fpkgFrtSmKJFwt9U6JWu9oYL0c9xWAzdAa+XQlmODiG6yw57AyOzetw6MKQUcgTEdxORuO37mWv6g==} + '@scalar/openapi-parser@0.28.16': + resolution: {integrity: sha512-zHEQExXo58Nk7/Tju4y4HLVJ+B5nnDmR7vPhhHLVinL0z99aGrd6S/8HREupVpfAsim326pfzIbSM+UOvEbnOw==} engines: {node: '>=22'} '@scalar/openapi-types@0.9.4': resolution: {integrity: sha512-eUSIjZEBLEF2i2pvcNdzXhzlKx6qt+hZsNklLmCyzPBoXWxHcQaEClgMFavXDRRvJDdi6LkyjqGUqqf0XgRgFg==} engines: {node: '>=22'} - '@scalar/openapi-upgrader@0.2.13': - resolution: {integrity: sha512-bPTkDsthDXj/3bgO7RDmRra1OfwxG/uQio6LP0h9Z8NCCgoZQ4E+V7S6bwF/9TWxbMRlb9djRNpbXCTh/L3C5g==} + '@scalar/openapi-types@0.9.5': + resolution: {integrity: sha512-czrz/zkVm1oPzrpYo3hI/iymfiw1s4dgJiQtwNi2U77Sqf3EQOGKsif4VNRa5suWMYRuPaFx5EiFM1FNEV4Whg==} + engines: {node: '>=22'} + + '@scalar/openapi-upgrader@0.2.15': + resolution: {integrity: sha512-yqROK9U96ElasEL4Wl/+PIjQZlqrQXVUUpXk9PA6xBnrp8KdEv7at8pR5gsZkvddJfYVwLILPlctyqUg4u4YZA==} engines: {node: '>=22'} '@sec-ant/readable-stream@0.4.1': @@ -2357,16 +2429,16 @@ packages: peerDependencies: svelte: ^5.0.0 - '@sentry/browser-utils@10.70.0': - resolution: {integrity: sha512-IvjhafF5NFXrPCg5EHbAPGDwIhHxgUcLlHTklZ3DAdA6ky88BJYMfevbcnOEmgavf4nylhCaquRUFNB5+szj+A==} + '@sentry/browser-utils@10.74.0': + resolution: {integrity: sha512-/Rh3WH4i2nStdOqHpaWocbpSMFPjKhlxKQgp9vkJBSolqjJzXlIpLYXOrldZHHZLG1iDoX3PFBQkNDYcPXCg0A==} engines: {node: '>=18'} - '@sentry/browser@10.70.0': - resolution: {integrity: sha512-IK6+J+8H06tZe+A8L37TT5ZxxwNtyQatW8zl5RYYJ/e9CsjrM8fPi8I1OT7uquTw8UtjqFHt7bEef/Vy63ksPg==} + '@sentry/browser@10.74.0': + resolution: {integrity: sha512-U6Zn+YcAgnR7bXZmpjMxivptPSsOeDKV+SlSqbJZqViOaCcZSG/vJiQr50UrdTccCwiU1LnwuOOoclJOvW+cLQ==} engines: {node: '>=18'} - '@sentry/bundler-plugins@10.70.0': - resolution: {integrity: sha512-M+a32VOZVeTUxF+QecQ+OHYj8hb5FeppKwwWpTQ9cfS6ixgoNUK7/B7NhB2XbfxE3F1BcGvOxyS+AB+fvPfSRQ==} + '@sentry/bundler-plugins@10.72.0': + resolution: {integrity: sha512-r9A2xZ/JlXuN/B6mfov+jn5RPrpz/35WqMtbns/WzwBDjMKQ5ZbsBUNFC/zXZ4VGEdhZCn0/gFFpl0yvt5NYiQ==} engines: {node: '>= 18'} peerDependencies: rollup: '>=3.2.0' @@ -2429,8 +2501,8 @@ packages: engines: {node: '>= 10'} hasBin: true - '@sentry/cloudflare@10.70.0': - resolution: {integrity: sha512-jlr9txw3lQrbc6KQn9FzlpgC1KcB36u7aIGSlUhXAZ2MB93DgwjuTnHF20qQHxmHbZylg7CtHoMq3rGiTXAgpg==} + '@sentry/cloudflare@10.74.0': + resolution: {integrity: sha512-7TgISdDF306CETi4iYEhKK8f5wY7ldLrYriFDrSQRvfGByBytleW8eduBpEgmtAFZQl8WDk8vRzqk0RZ9is0gA==} engines: {node: '>=18'} peerDependencies: '@cloudflare/workers-types': ^4.x || ^5.x @@ -2445,16 +2517,20 @@ packages: resolution: {integrity: sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==} engines: {node: '>=14'} - '@sentry/core@10.70.0': - resolution: {integrity: sha512-ozhCTDqg89oB4XmWfAwuHshABpvT7AkRpaPnogopPfMAaI61G1t8EKCJ4W7aum8JSBonlfyjPCyW5oYZFm0KvA==} + '@sentry/core@10.72.0': + resolution: {integrity: sha512-UJMHZfbjP4qk+g4AQhZmzosMdICC2D9p0/hLrm1LofPsp+WBfcSnv9jsY1a9TfmpS4WCAmTx8nANYTIXifcBjA==} + engines: {node: '>=18'} + + '@sentry/core@10.74.0': + resolution: {integrity: sha512-u9rY8vcZfktccwm6LznfCZlqP5C9A+p76r4/pFS1grqpuTO0m21Cl8rosnlESrDGP/Xd9tfr91rWYk0jPH8jeQ==} engines: {node: '>=18'} - '@sentry/feedback@10.70.0': - resolution: {integrity: sha512-6VQn2ETJjHkk4QQDdx/587/JDfXsk3yBTLZ3UZOMtBVrkmwgk+1FJZ8ULJ3ud1xZcuh2icunIkc7tGVv2axdnw==} + '@sentry/feedback@10.74.0': + resolution: {integrity: sha512-WwUao9K2XgRIxTUTaBhm7aglhsyP7LQTGi2lxvFbXzlSagUzwew/EL2c5oqQ23Mjn94i5SY+rM4wMdgyWdtpAw==} engines: {node: '>=18'} - '@sentry/node-core@10.70.0': - resolution: {integrity: sha512-oPOEVVNxv5WHtckx2i06Wi9FLWyvOg/1DUeX732jZ4iqT2nupINaMH4nF4f4kSvUThFnxkFSRQxwqOxgzMKhKA==} + '@sentry/node-core@10.74.0': + resolution: {integrity: sha512-btgZXcGmkOGgojbxHo/gfGiyqYpzC9E8zVR78Q3MtM6Xnemk9gwJiQlhNmEX/FM+C36WBRPZrdcZcnMaJhbfJw==} engines: {node: '>=18'} peerDependencies: '@opentelemetry/api': ^1.9.0 @@ -2474,38 +2550,38 @@ packages: '@opentelemetry/sdk-trace-base': optional: true - '@sentry/node@10.70.0': - resolution: {integrity: sha512-SPOOVxmKTVIEtqvOKkQT163e/pOwucjS7OPsCHyRs8sFR4nfBNu0EThplyqnvqd5BWBMTPH6WTBQfo+QWHV+HA==} + '@sentry/node@10.74.0': + resolution: {integrity: sha512-u1wsarTOWHn9CCev81Da5T4IQHZgdcosXRfX2+4DMII/lVJMBYesKixTQuMwXexjVG2+pkf16zTmgjZDa+75jA==} engines: {node: '>=18'} - '@sentry/opentelemetry@10.70.0': - resolution: {integrity: sha512-UNV/2tqypcUK6FDzerAsFJn1Km/c4VZCYkUZDNbnV5S0cwAq2BYKMo4M5vovaLDBQlxA+Wk9ovbxi5wYjjl9fw==} + '@sentry/opentelemetry@10.74.0': + resolution: {integrity: sha512-ua5mt0NDBfye+/ACKjAw9Ad2i+y42lOLvhxXZepZXjszblMz80MEsIZflMB7uLZUCTNH7MbZN8tlzCy8KsJKmQ==} engines: {node: '>=18'} peerDependencies: '@opentelemetry/api': ^1.9.0 '@opentelemetry/core': ^1.30.1 || ^2.1.0 '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 - '@sentry/replay-canvas@10.70.0': - resolution: {integrity: sha512-irzpw22bK5CF3jbecDa0gBUcfjv7tgeUoLAvtIfeHOP5ajmf3o4Cp99a9RQqkfgvkcMeRZBUwE91SQhp6Ank+w==} + '@sentry/replay-canvas@10.74.0': + resolution: {integrity: sha512-HAdu4ViRsuOD5ifphQR24HugL0geZXV8LnGQVkA8vlzZWiuYgGNqDRoUWI75IgVcCYLYSaVBoKN+jTimdzG/sQ==} engines: {node: '>=18'} - '@sentry/replay@10.70.0': - resolution: {integrity: sha512-xMnSGzJn9Xd29rYd32lkx/gFW+5mtgqADJ2FiZvis0MBGZuDlNRwPn0/Cs1xA3JNXKV3NGfhdmmvs90w4dHSmw==} + '@sentry/replay@10.74.0': + resolution: {integrity: sha512-amIu0zKzhQJZoWmlwfFnn/uKRGyF+Tikph3lBgYwj5XbHv4U94PQXi9Ll8CYzzLVS7BkrCVc2Hl7RvpRg5M2Yg==} engines: {node: '>=18'} - '@sentry/server-utils@10.70.0': - resolution: {integrity: sha512-rzegZjMFFgCp3o+N8+XU13rfSvz4B+f8rU0ijBGrQcHdMNyfsFDTu1UTm262JofmrV2u+s+D0u0vFTnqtOGkbA==} + '@sentry/server-utils@10.74.0': + resolution: {integrity: sha512-AHmPIGE8yVRyywpZRhAkm/H0cHTgsQSPhFZFbaeQLcYHFE/eOP9bAMh8Nj/Apr7WMagFXYHd04tR/63BxtLgKw==} engines: {node: '>=18'} - '@sentry/svelte@10.70.0': - resolution: {integrity: sha512-AX7Xm15oHD3oVgw1Rx0zohYy5XEsrbjFdDIvkqPs3p3lEeLTMOyXO9PEBKw+c4yhfJADS8QDU1Y1wuZAonGSkw==} + '@sentry/svelte@10.74.0': + resolution: {integrity: sha512-sI1q3zkP2aBL8CyyJ9rTPthJi9OiJ2cDwC/htmmX6DtjUJOPPa/tBN/FnzNffUGO1++wLkyRUDxWbqG5DayOMQ==} engines: {node: '>=18'} peerDependencies: svelte: 3.x || 4.x || 5.x - '@sentry/sveltekit@10.70.0': - resolution: {integrity: sha512-UF9a/UniSOoBzmUby0bfvzoBjP496M1m6n5vvdXB+HKcQv/NtOxaPl1dV8OsFYkpJzREQ+uEX5493rHIxK9tUw==} + '@sentry/sveltekit@10.74.0': + resolution: {integrity: sha512-Ql5zID1nLGP/2XhoIow3lYz050IY+kKWPNOKDSHRQWTOwawbbmKA2FTDxZMenaaoJZlWmc/x29T3b6weNMvCfg==} engines: {node: '>=18'} peerDependencies: '@sveltejs/kit': 2.x || ^3.0.0-0 @@ -2694,59 +2770,60 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 - '@takumi-rs/core-darwin-arm64@2.10.0': - resolution: {integrity: sha512-6FipAj/BzRZeJbuDdeOavI93r83jw3jZTYgW/LAtn8syYjxB2Ayxx2hY4HJKdZK2KDVHvkr7ZENEbWqGtSAdqw==} + '@takumi-rs/core-darwin-arm64@2.13.7': + resolution: {integrity: sha512-Jbe1ik4bcVOOHDpa5RCjm5fe+LfJjjyebvvPBMekhEKXg6hlTbIEFmhr/GCxMOKUfaNsvb4amZ/bVCo0NDSCjA==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@takumi-rs/core-darwin-x64@2.10.0': - resolution: {integrity: sha512-2FbQZr3l7N66/PHBaVZBj8HEVVVAGvVzJaVtBCenUQDRnsaE0F7VkFovllYpuVHwpnIuYH3qTXZL90mB/Weoeg==} + '@takumi-rs/core-darwin-x64@2.13.7': + resolution: {integrity: sha512-PvmSmhjbSosM3XaHZ9VhFm1x7Xc9oZ5GqXlskiwZblKIi5S4EI43buhUd28xzcBlKkB/vC3c15Cj417XjnUk4g==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@takumi-rs/core-linux-arm64-gnu@2.10.0': - resolution: {integrity: sha512-n1L/ZBfJ0ctCjwhBdL+5RMIRZOUQiKplmxUT774ofgVDsxIuXz84NF/mqcPWMfIPU5qtCEcHysUUbXZirEYK+A==} + '@takumi-rs/core-linux-arm64-gnu@2.13.7': + resolution: {integrity: sha512-TGvDidIF3dtb7TMoJRF+0BET/v6eAuQxV8AXo/57GqO3TM9wjLt3AbkrFkzxSsaIRXlBQxCfb+GsS0YdGnpo4w==} engines: {node: '>=18'} cpu: [arm64] os: [linux] libc: [glibc] - '@takumi-rs/core-linux-arm64-musl@2.10.0': - resolution: {integrity: sha512-Mju5+Rj4xv/M+CFtWzvmkcziPFl5QTYAXpb0Y7mRn7vQfRCv06gh628a8XVrkCu45RsyTR535GRUM0oV9n3WGQ==} + '@takumi-rs/core-linux-arm64-musl@2.13.7': + resolution: {integrity: sha512-CL92+JeFbqlPfWI2SSQqLtcjWpxEsPYQ+wNhaqUWLnlTMfZfj+Fl3ERFLbmK+lYnGs7bzpPXLisd3GJQ37rTYw==} engines: {node: '>=18'} cpu: [arm64] os: [linux] libc: [musl] - '@takumi-rs/core-linux-x64-gnu@2.10.0': - resolution: {integrity: sha512-fqzgmp60yefxVfqzRw0n6VOeE6jHEI7zHgPUMzxs7bF8SfrX+8uagvzBog5aHrSB5AefhsVboWgqlx9Wx+S0Yw==} + '@takumi-rs/core-linux-x64-gnu@2.13.7': + resolution: {integrity: sha512-ZxoapyUDlInik93WIA9+Sk/ByOb3QGO7GaL9PA7OOdIlK5ovK9v9q88tZb1cvfxtIVzdE9HPBUhb5quNMcuXWg==} engines: {node: '>=18'} cpu: [x64] os: [linux] + libc: [glibc] - '@takumi-rs/core-linux-x64-musl@2.10.0': - resolution: {integrity: sha512-wx0Poj7dSIEczsyXdeo4XvDHJaCxiJiXwojsVbQQ3LwLRlLN3wRYxW4Ujx090XFXgy/WQ4Rip0/CgATs3h/zQQ==} + '@takumi-rs/core-linux-x64-musl@2.13.7': + resolution: {integrity: sha512-OqoVTd7mrlIwjKYYFizUwz+3zpnVsbrpLFPXFC+BmUxdFYNZwKa0R+Pk56EslyBZS3CuBy+RnBusHCvNC7+09g==} engines: {node: '>=18'} cpu: [x64] os: [linux] libc: [musl] - '@takumi-rs/core-win32-arm64-msvc@2.10.0': - resolution: {integrity: sha512-1n5Dorkx3tq5Z/NVSB+HcopiLaMF60kf8ncVMG/pHaui1JdmWyJb6lDud7PrH3354XwEkVHhdR/9yEVezhGL4Q==} + '@takumi-rs/core-win32-arm64-msvc@2.13.7': + resolution: {integrity: sha512-evSyi/y3uV03U+PJfLeG2V3aRsmmWJ6elrFpkLtHebWwi49kXazTL5TYsCkPj5hA4i+rNs6z/t/BygLj1Krp8Q==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@takumi-rs/core-win32-x64-msvc@2.10.0': - resolution: {integrity: sha512-FnjkXva7vdaZO7TvF/Z8cs1UXjf9XdgCv3CIo4QHPx4NgxaTZqbod+K3xH3FFpZjQhnWLoHvTDRntWiLdUzpag==} + '@takumi-rs/core-win32-x64-msvc@2.13.7': + resolution: {integrity: sha512-GawtljFxuJemQynlF5wryxtSLv18XNuKSjMt52xf66WMaBKt4eN6/b7yIZp9QqKPof8ip8uqFOt7fWVSUN+gHQ==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@takumi-rs/core@2.10.0': - resolution: {integrity: sha512-+2/648UWDgl4UrRKORYp9GVl5epWV90uxAxSbPSgB+bY/kqYpYekEKvq/76NOjjb7HldvO2IMmzSu6Y60C1KkQ==} + '@takumi-rs/core@2.13.7': + resolution: {integrity: sha512-FBkglt8kGBZwT2CHr+kppN+FH72Oy8gLIZz1BIbD9vNcocLrvtLsh1Y0nB18mOPmaROKGUVUXLPnIuWo49CRng==} engines: {node: '>=18'} peerDependencies: csstype: '*' @@ -2754,8 +2831,8 @@ packages: csstype: optional: true - '@takumi-rs/helpers@2.10.0': - resolution: {integrity: sha512-cPQNj/16DASgMaIA07ZrjNQ14Z+7lWiVPZQVz5S1kLAhVlCf9qg2D2vge/+yglHrdaL556U22wDWNUyFId5a+A==} + '@takumi-rs/helpers@2.13.7': + resolution: {integrity: sha512-6AKk/ixuvoCEZD9/LPAz3kxTcwWHeZh15quhHIHYiVxFRl4x2LyKN8j6eZngiepYYsORXAuJ5YlTGA7uIRfeMQ==} engines: {node: '>=18'} peerDependencies: preact: ^10.0.0 @@ -2766,9 +2843,9 @@ packages: react: optional: true - '@takumi-rs/wasm@2.10.0': - resolution: {integrity: sha512-euimMzmiWeSDuWYRTCMx6V3aBMj3zAjIwx47StvDWuQKK3eDBaqKbrE1VCe9MHH4l1Mne1ZQpLUmLQeJwmZ/2w==} - engines: {node: '>=18'} + '@takumi-rs/wasm@2.13.7': + resolution: {integrity: sha512-tc2IFETJ9HtZpS94cL3qSTiecJhhR4RUTXbb3E8cyz6WKdyoZSFkL2r67xnidVtcv22RKTyfVJ2gxV7eWvF+mA==} + engines: {node: '>=20.19'} peerDependencies: csstype: '*' peerDependenciesMeta: @@ -2836,8 +2913,8 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@26.2.0': - resolution: {integrity: sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==} + '@types/node@26.5.1': + resolution: {integrity: sha512-CzNm2FezW4VR/LjG6yUdiEgLE/rAQ9Slj5gCu/C2VrdcW7I0ahNZ8DRbHT7zOZ6r3ONgd/bsQIeSaoDGrd1C6g==} '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -2908,63 +2985,63 @@ packages: '@types/json-schema': optional: true - '@typescript-eslint/eslint-plugin@8.67.0': - resolution: {integrity: sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==} + '@typescript-eslint/eslint-plugin@8.70.0': + resolution: {integrity: sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.67.0 + '@typescript-eslint/parser': ^8.70.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.67.0': - resolution: {integrity: sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==} + '@typescript-eslint/parser@8.70.0': + resolution: {integrity: sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.67.0': - resolution: {integrity: sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==} + '@typescript-eslint/project-service@8.70.0': + resolution: {integrity: sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.67.0': - resolution: {integrity: sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==} + '@typescript-eslint/scope-manager@8.70.0': + resolution: {integrity: sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.67.0': - resolution: {integrity: sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==} + '@typescript-eslint/tsconfig-utils@8.70.0': + resolution: {integrity: sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.67.0': - resolution: {integrity: sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==} + '@typescript-eslint/type-utils@8.70.0': + resolution: {integrity: sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.67.0': - resolution: {integrity: sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==} + '@typescript-eslint/types@8.70.0': + resolution: {integrity: sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.67.0': - resolution: {integrity: sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==} + '@typescript-eslint/typescript-estree@8.70.0': + resolution: {integrity: sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.67.0': - resolution: {integrity: sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==} + '@typescript-eslint/utils@8.70.0': + resolution: {integrity: sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.67.0': - resolution: {integrity: sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==} + '@typescript-eslint/visitor-keys@8.70.0': + resolution: {integrity: sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@valibot/to-json-schema@1.7.1': @@ -3063,11 +3140,6 @@ packages: ajv-formats@3.0.1: resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true ajv@6.15.0: resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} @@ -3114,16 +3186,13 @@ packages: ast-v8-to-istanbul@1.0.5: resolution: {integrity: sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==} - astring@1.9.0: - resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} - hasBin: true - atomic-sleep@1.0.0: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} - auth@1.6.29: - resolution: {integrity: sha512-/VaULTixl7G8L75OK2vDtcOIyeTEEHaKZlBoZ0q80SAFD+ToLyo5cJYxxkkqyA2al0luFUMTzndSdU+RHIr/wA==} + auth@1.7.7: + resolution: {integrity: sha512-Iuka9UTqPUI4qQORHjZ0U4D/ceH3q11e1iGU+j2N/Flfy2jQK/vGACzrBBXDArE7kFAn468g/P8+aAOonU8s0A==} + engines: {node: '>=22.12.0'} hasBin: true axobject-query@4.1.0: @@ -3143,17 +3212,16 @@ packages: engines: {node: '>=6.0.0'} hasBin: true - better-auth@1.6.29: - resolution: {integrity: sha512-7i2kOry+Jb1mRUR14kCDVS23GCKUGWxayZRXULbNz/6nFLPT6XSuGJVrwo/d+Qfq/iubA0/4Acw2/eg/tPCCnA==} + better-auth@1.7.7: + resolution: {integrity: sha512-Zhgxi/c5ylmfB/sX+nwnBbsFf/j6k0L8DvL6CJXZSrXgxc5pPMC/e7j812zNd4L4B4ELmdURCPw3X8nAOUdcjw==} peerDependencies: '@lynx-js/react': '*' '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 '@sveltejs/kit': ^2.0.0 '@tanstack/react-start': ^1.0.0 '@tanstack/solid-start': ^1.0.0 - better-sqlite3: ^12.0.0 - drizzle-kit: '>=0.31.4' - drizzle-orm: ^0.45.2 + drizzle-kit: '>=0.31.4 || >=1.0.0-beta.1' + drizzle-orm: ^0.45.2 || >=1.0.0-rc.1 <2.0.0 mongodb: ^6.0.0 || ^7.0.0 mysql2: ^3.0.0 next: ^14.0.0 || ^15.0.0 || ^16.0.0 @@ -3163,7 +3231,7 @@ packages: react-dom: ^18.0.0 || ^19.0.0 solid-js: ^1.0.0 svelte: ^4.0.0 || ^5.0.0 - vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 + vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 vue: ^3.0.0 peerDependenciesMeta: '@lynx-js/react': @@ -3176,8 +3244,6 @@ packages: optional: true '@tanstack/solid-start': optional: true - better-sqlite3: - optional: true drizzle-kit: optional: true drizzle-orm: @@ -3217,8 +3283,8 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} - bits-ui@2.19.0: - resolution: {integrity: sha512-dHDm5Jw2NZJgLRvHAwPeuR4BnZij+AqTIq33OqVfo+qiMXontoC/yIvGT5TZaviv7OuBdJ2QUy2JPQ2yKBvF6w==} + bits-ui@2.19.2: + resolution: {integrity: sha512-W9upilTmLTIWjuVAD6Ta7XoS6gxRflmCQnXamj+WN1eYxZecghqGENpuSMAKQmVENMLXhh85BZb04LCrBd2+Ng==} engines: {node: '>=20'} peerDependencies: '@internationalized/date': ^3.8.1 @@ -3258,11 +3324,23 @@ packages: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} - c12@3.3.4: - resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==} + c12@4.0.0-beta.5: + resolution: {integrity: sha512-yWGCPCQGJeFq4R0mFg5HOhC3Rg+B0PCdM+ldXWUhughoGgeeq8/tjRmXh4/lmhKWyhf+KOFxB/JMXf0Yv1Fd5A==} peerDependencies: + chokidar: ^5 + dotenv: '*' + giget: '*' + jiti: '*' magicast: '*' peerDependenciesMeta: + chokidar: + optional: true + dotenv: + optional: true + giget: + optional: true + jiti: + optional: true magicast: optional: true @@ -3270,6 +3348,9 @@ packages: resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} engines: {node: '>=20.19.0'} + cacheable@2.5.0: + resolution: {integrity: sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -3310,8 +3391,9 @@ packages: charenc@0.0.2: resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} - chevrotain@10.5.0: - resolution: {integrity: sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==} + chevrotain@12.0.0: + resolution: {integrity: sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==} + engines: {node: '>=22.0.0'} chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} @@ -3349,10 +3431,6 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} - commander@12.1.0: - resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} - engines: {node: '>=18'} - commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} @@ -3376,12 +3454,12 @@ packages: console-table-printer@2.12.1: resolution: {integrity: sha512-wKGOQRRvdnd89pCeH96e2Fn4wkbenSP6LMHfjfyNLMbGuHEFbMqQNuxXqd0oXG9caIOQ1FTvc5Uijp9/4jujnQ==} - conventional-changelog-angular@9.3.0: - resolution: {integrity: sha512-0MWQLVUT1oVCsUGs9aAWteBVxPlLwJTn5VbQH7B0B3fDizZgrJ9QGnKl/2mp1+5P7153GCBCjO/v1aKJ6eysCg==} + conventional-changelog-angular@9.4.0: + resolution: {integrity: sha512-HdxRxuS8bBXVIuo4V82gvSwAXT0vYQUizrjs/izmPg5JdDstr8v8I5hduGL3iQbG+o310dUDxC4+LetuS5hu9w==} engines: {node: '>=22'} - conventional-changelog-conventionalcommits@10.3.0: - resolution: {integrity: sha512-qag0zFD867Qq1DK0jAWicyWlEMS1FFC/BLVLISaoeI7Y6Em6aWchk7BCkhOTsecRpMsV6qX41XmlNnghiiTmSw==} + conventional-changelog-conventionalcommits@10.4.0: + resolution: {integrity: sha512-Rriac6ZrAlVm6cy9Bz4NSp+WMHpwNXoPIYex+HjCgduAVUSbnew29DQjQw0C4g9u3HtSYzGiGY+pdBXAZo+4aA==} engines: {node: '>=22'} conventional-commits-parser@7.1.2: @@ -3467,8 +3545,8 @@ packages: dateformat@4.6.3: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} - dayjs@1.11.21: - resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} + dayjs@1.11.23: + resolution: {integrity: sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==} debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} @@ -3493,8 +3571,8 @@ packages: resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} engines: {node: '>=18'} - default-browser@5.5.0: - resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + default-browser@5.5.1: + resolution: {integrity: sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==} engines: {node: '>=18'} define-lazy-prop@3.0.0: @@ -3515,8 +3593,8 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - devalue@5.9.1: - resolution: {integrity: sha512-+17vil3EVQRzvtDJSFuTWEb8XJRvXqAiV3qZyQWD398QeXUa6CxsUyMdD1fxzEhUrd4FojitFz7lhIHBTlV4fw==} + devalue@5.9.2: + resolution: {integrity: sha512-po4PAY5c53tw5XMocSnf8A/5OHhbbUftpr93aEN6BBoAdntUmK7vu7wOATqvt7cXO7m1Cl4gMVn6p7n6n4mj0w==} devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -3549,12 +3627,12 @@ packages: resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} - drizzle-kit@0.31.10: - resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==} + drizzle-kit@0.31.11: + resolution: {integrity: sha512-YCYqxTLIB2OCqf6w9/Vef13baBVbwQIPcbT4Y56AfO6B9ajZhDhD8Jkveg/hJvT/iuMloKD/IYYkyMMNhr7Kqg==} hasBin: true - drizzle-orm@0.45.2: - resolution: {integrity: sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==} + drizzle-orm@0.45.4: + resolution: {integrity: sha512-NiM1YZpJ2UxyeE67DpDDZ+Tsw4YLX3trSlkNT7Tm9eKoUNb1LvaAaQzXgYQsWs+AQ9TUmmwMmBOCXuqqxUWKKQ==} peerDependencies: '@aws-sdk/client-rds-data': '>=3' '@cloudflare/workers-types': '>=4' @@ -3562,6 +3640,7 @@ packages: '@libsql/client': '>=0.10.0' '@libsql/client-wasm': '>=0.10.0' '@neondatabase/serverless': '>=0.10.0' + '@netlify/db': '>=0.4.0' '@op-engineering/op-sqlite': '>=2' '@opentelemetry/api': ^1.4.1 '@planetscale/database': '>=1.13' @@ -3598,6 +3677,8 @@ packages: optional: true '@neondatabase/serverless': optional: true + '@netlify/db': + optional: true '@op-engineering/op-sqlite': optional: true '@opentelemetry/api': @@ -3680,14 +3761,11 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-module-lexer@2.3.1: - resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} - es-module-lexer@2.3.2: resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} - es-toolkit@1.50.0: - resolution: {integrity: sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==} + es-toolkit@1.52.0: + resolution: {integrity: sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==} esbuild@0.18.20: resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} @@ -3751,8 +3829,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.8.1: - resolution: {integrity: sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==} + eslint@10.10.0: + resolution: {integrity: sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -3776,8 +3854,8 @@ packages: resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} - esrap@2.3.5: - resolution: {integrity: sha512-KciPkeZZX8srrh6xELzLR2cBaWOzgBQ4CwggO6Dh/KMlrfOcIcfyXCVy/Vvc3/OlS5gvFOd5tcxUYuaGM8o45A==} + esrap@2.3.6: + resolution: {integrity: sha512-yc0OC12UjPqLoc+fe+v5GNs4TOjAigUw3sTikfC+xeBPGUw7gDRz3DtYaqEhxyMVJojcSWJw7jT0QWR+CbuE/A==} peerDependencies: '@typescript-eslint/types': ^8.2.0 peerDependenciesMeta: @@ -3844,8 +3922,8 @@ packages: fast-string-width@3.0.2: resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} - fast-uri@3.1.5: - resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} + fast-uri@3.1.6: + resolution: {integrity: sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==} fast-wrap-ansi@0.2.2: resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} @@ -3869,9 +3947,8 @@ packages: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} - file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} + file-entry-cache@11.1.5: + resolution: {integrity: sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==} file-type@21.3.4: resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} @@ -3892,9 +3969,8 @@ packages: resolution: {integrity: sha512-JGG8pvDi2C+JxidYdIwQDyS/CgcrIdh18cvgxcBge3wSHRQOrooMD3GlFBcmMJAN9M42SAZjDp5zv1dglJjwww==} engines: {node: '>=20'} - flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} + flat-cache@6.1.23: + resolution: {integrity: sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==} flatted@3.4.4: resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} @@ -3913,11 +3989,6 @@ packages: resolution: {integrity: sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==} engines: {node: '>=14.14'} - fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -3942,8 +4013,8 @@ packages: resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} engines: {node: '>=18'} - get-tsconfig@4.14.2: - resolution: {integrity: sha512-XpwZALwwl/BaKTAyC6+c5T8y6kCg2jk+XGqOVrKIQmW49pNypYLMRjCUXqa28tQgJlhS2RlzP7sc+Rx7W6qsfw==} + get-tsconfig@4.14.3: + resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==} get-tsconfig@4.8.1: resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==} @@ -3972,8 +4043,8 @@ packages: resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} - globals@17.11.0: - resolution: {integrity: sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==} + globals@17.12.0: + resolution: {integrity: sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==} engines: {node: '>=18'} globalyzer@0.1.0: @@ -4004,14 +4075,18 @@ packages: resolution: {integrity: sha512-cQOsSMS/IrDz82PVyRDvf/Q1F/bRbBVjJlh+xYOkI1qw2bWRvWGiWc+m2O0d6l4Bt1fyY+8kzJ8JFWGJqNeDBg==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} - happy-dom@20.11.2: - resolution: {integrity: sha512-7MB+bJLkxu3SowAfBJbjW+c55kNz5tkR45gu2qzrxznezhLeN5YIlJbwUgSzlGc+qWoZ8Ykg71H5ezz69xixrw==} + happy-dom@20.12.0: + resolution: {integrity: sha512-7uMYJu2SEwwL8vVcKp0C0lnt6d2LSGGe+T+oY79PiCJNNSgFpbxW8n5KuzpDQvrU4mt+fYiK1+Jy7Z2v39YR6g==} engines: {node: '>=20.0.0'} has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + hashery@1.5.1: + resolution: {integrity: sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==} + engines: {node: '>=20'} + hasown@2.0.4: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} @@ -4022,6 +4097,12 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + hookified@1.15.1: + resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} + + hookified@2.2.0: + resolution: {integrity: sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==} + html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -4125,6 +4206,10 @@ packages: is-hexadecimal@2.0.1: resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} engines: {node: '>=14.16'} @@ -4188,14 +4273,14 @@ packages: jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} - joi@17.13.4: - resolution: {integrity: sha512-1RuuER6kmt8K8I3nIWvPZKi5RQCb568ZPyY4Pwjlua+yo+63ZTmIwxLZH0heBmiKN4uxjvCiarDrjaeH84xicQ==} + joi@17.13.6: + resolution: {integrity: sha512-ImNZaq/LSysofih+xIGYfR0WUXMA9GLUNB//YTCSrZptoRmVgaNAdJyi6K1kXi9pkLEoSkoI8I4UwtNiu/D7nw==} jose@5.10.0: resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} - jose@6.2.8: - resolution: {integrity: sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ==} + jose@6.2.10: + resolution: {integrity: sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==} joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} @@ -4207,8 +4292,8 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@4.3.1: - resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} hasBin: true jsesc@3.1.0: @@ -4216,9 +4301,6 @@ packages: engines: {node: '>=6'} hasBin: true - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} @@ -4259,8 +4341,8 @@ packages: resolution: {integrity: sha512-9Ag50tKhpTwS6r5wh3MJSAvpSof0UBr39Pto8OnzFT32Z/pAbxAsKHzyvsyMEHVslELvHyO/4/jaQELHk8wDcw==} hasBin: true - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + keyv@5.6.0: + resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} @@ -4273,8 +4355,8 @@ packages: known-css-properties@0.37.0: resolution: {integrity: sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==} - ky@2.0.2: - resolution: {integrity: sha512-/GmXpo9F9W+f8n4Ivr2iH+7h7wL7jLbLKWkMlpflcCRb6kGjBfTlASEXaZ9qUgNTn4VgS0P2pwxxzQ4EM6Ulgg==} + ky@2.1.0: + resolution: {integrity: sha512-nIKwelw+7qVqKOlX4Eht6GXfEXlDHcdIjyLWV3s119kYt5s+70ayTnYS+0fRNmtgGRRAfqdeWQbh8YizqkS0ng==} engines: {node: '>=22'} kysely@0.29.5: @@ -4300,8 +4382,8 @@ packages: engines: {node: '>=16'} hasBin: true - libphonenumber-js@1.13.11: - resolution: {integrity: sha512-ETER2kMaIFTI/Nh1a8Gk03dUF/SL0VZqtI+CcVHZxp5WIHYwNS7S+uiYZDYCvLy3lOR4/DAD5jf0h5WkePPpqg==} + libphonenumber-js@1.13.12: + resolution: {integrity: sha512-uLVeV1c9OTk6qkdqnj+mpMD+ZdnZ0szVyWu58HwMmpwkHA1gCEkyjd3veZQXDnuw9KEwSRjcc9B1pS9XKIN1fA==} lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} @@ -4472,9 +4554,6 @@ packages: resolution: {integrity: sha512-XT9ewWAC43tiAV7xDAPflMkG0qOPn2QjHqlgX8FOqmWa/rxnyYDulF9T0F7tRy1u+TVTmK/M//6VIOye+2zDXg==} engines: {node: '>=20'} - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} @@ -4502,8 +4581,8 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - magic-string@1.2.0: - resolution: {integrity: sha512-ptco+HFxTLgjafSLim2LojBSwfg5feBjd+SqyiwdGkzC38UPdZy3zgrHMI2CoTf5fJL38tbHMYWVzIH8BxGqJw==} + magic-string@1.2.3: + resolution: {integrity: sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g==} magicast@0.5.4: resolution: {integrity: sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==} @@ -4512,8 +4591,8 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} - markdown-it@14.3.0: - resolution: {integrity: sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==} + markdown-it@14.3.1: + resolution: {integrity: sha512-4Ej49aYTDFIQ+uBkfX8GBvJGccoARxxPep+7aWTs55ozbjQJpW9M26Fe53vnGgvLeVzva/amzjQQaQu9w0vMhA==} hasBin: true marked@14.0.0: @@ -4551,10 +4630,6 @@ packages: memoize-weak@1.0.2: resolution: {integrity: sha512-gj39xkrjEw7nCn4nJ1M5ms6+MyMlyiGmttzsqAUsAKn6bYKwuTHh/AO3cKPF8IBrTIYTxb0wWXFs3E//Y8VoWQ==} - meriyah@6.1.4: - resolution: {integrity: sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==} - engines: {node: '>=18.0.0'} - meshoptimizer@0.18.1: resolution: {integrity: sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==} @@ -4668,8 +4743,8 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - nanostores@1.4.2: - resolution: {integrity: sha512-Wxv8Roefr2nqtiRG0bnaFlpYqpIVtOEeJZHaH+4nGgOK1/7n6OHOuHCb/bhqrNQgZM8fyd0s1PqhdrJc9Ib44g==} + nanostores@1.5.2: + resolution: {integrity: sha512-B0UbxzK1s0CN8Xht6r+7iT5+xV8PTaRERR1nATeplRv1Rw5YLWfVAid0hkqY3EceqpG4RjTk8GAwIxQY39Rnwg==} engines: {node: ^20.0.0 || >=22.0.0} natural-compare@1.4.0: @@ -4738,9 +4813,6 @@ packages: resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} - ohash@2.0.12: - resolution: {integrity: sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==} - on-exit-leak-free@2.1.2: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} @@ -4748,16 +4820,16 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - open@10.2.0: - resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} - engines: {node: '>=18'} + open@11.0.2: + resolution: {integrity: sha512-RWqF+pBSkqecEvCKOn8QYhaNdRMJDZRIrlS/7rTDdLHaPcfXGCZ/h8zb413NfvdeAV0MR7T1yJcA34/q+CSm1Q==} + engines: {node: '>=20'} optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - orval@8.24.0: - resolution: {integrity: sha512-dtrFTGJQCtTpvsFk7BEKzNsvjYW8SD0hFUyoC2dL4bWVHfKmujRWeMYgm3h9U/cJPZBpgG4UqLnfVY8/DbZpYw==} + orval@8.31.0: + resolution: {integrity: sha512-hTX8pVmlRjuewGKhOPgsyFZBk4pS64d66oix0MK7bL5z+OGCOyJNBqxw0F693YsWd8G/Sk04S3SmOHm6Yupdsw==} engines: {node: '>=22.18.0'} hasBin: true peerDependencies: @@ -4766,8 +4838,8 @@ packages: prettier: optional: true - oxfmt@0.64.0: - resolution: {integrity: sha512-XZ4GFBN/PLbXKq+0zrgpQfPKYuJlUuj+nzZJY7UpIbFMNyefNLCdN9EwViycNqnYcv0wrn0jXcQLlqJp8RCKBg==} + oxfmt@0.67.0: + resolution: {integrity: sha512-vV7sSiPsaO0mSxdoUdayipVDFPzW/UQ+hrezEHa20+Tx1dnMdZLSRHMT0PdS67FFbhd74M1n08asW21aLGeCrA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -4847,16 +4919,13 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - payload@3.88.0: - resolution: {integrity: sha512-O7zuS80bvEGLte+7xZjwN05+ox5BCsGcQT2M6+CTote07JQOOvHJoiuoyQFw6cUElcFTWGMC5dy03w7J7sTYGg==} + payload@3.89.0: + resolution: {integrity: sha512-n0rDsMDTlKkNDqNllSLow1xEn2XaktD2oyi6Gl7i/LAD7xRBaS+tj27ca9EeNM5QmoGSd734UCHYj5Vr4JQYNA==} engines: {node: ^18.20.2 || >=20.9.0} hasBin: true peerDependencies: graphql: ^16.8.1 - perfect-debounce@2.1.0: - resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} - pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} @@ -4875,8 +4944,8 @@ packages: resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@4.0.5: - resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} engines: {node: '>=12'} pino-abstract-transport@2.0.0: @@ -4896,13 +4965,13 @@ packages: pkg-types@2.3.1: resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} - playwright-core@1.62.1: - resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} + playwright-core@1.63.0: + resolution: {integrity: sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==} engines: {node: '>=20'} hasBin: true - playwright@1.62.1: - resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} + playwright@1.63.0: + resolution: {integrity: sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==} engines: {node: '>=20'} hasBin: true @@ -4950,6 +5019,10 @@ packages: resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} @@ -4970,6 +5043,14 @@ packages: resolution: {integrity: sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw==} engines: {node: '>=12'} + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} + engines: {node: '>=20'} + + powershell-utils@0.2.1: + resolution: {integrity: sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==} + engines: {node: '>=20'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -4979,8 +5060,8 @@ packages: engines: {node: '>=14'} hasBin: true - pretty-ms@9.3.0: - resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + pretty-ms@9.3.1: + resolution: {integrity: sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==} engines: {node: '>=18'} prismjs@1.30.0: @@ -5021,6 +5102,10 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + qified@0.10.1: + resolution: {integrity: sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==} + engines: {node: '>=20'} + qs-esm@8.0.1: resolution: {integrity: sha512-eZ7l+0ZVy3He9c85pEM9KEBR9DFA4jrmWvIjm9wpcHvScwc/vgZDl2TNOF0pm0JsWKw24XBUZOY0Wxn7/nvJnw==} engines: {node: '>=18'} @@ -5051,10 +5136,14 @@ packages: peerDependencies: react: '>=16.13.1' - react-error-boundary@6.1.2: - resolution: {integrity: sha512-3DpCr5HVdZ0caUjYE/kIHBEJN0mNP3ZCgf16c48uJ5TbWjorKVp+YG8W3XqlJ7vJAVNw6wNIImyPXmFydwmyng==} + react-error-boundary@6.1.3: + resolution: {integrity: sha512-GnSKpCohFi2nQmJCWwP8O8wub7zexlePvpsejvQr35vS5RTouS1+utTNOmyc540yw5vyOXnSL1rBWsCQDmkyUA==} peerDependencies: + '@types/react': ^18.0.0 || ^19.0.0 react: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true react-image-crop@10.1.8: resolution: {integrity: sha512-4rb8XtXNx7ZaOZarKKnckgz4xLMvds/YrU6mpJfGhGAsy2Mg4mIw1x+DCCGngVGq2soTBVVOxx2s/C6mTX9+pA==} @@ -5096,11 +5185,8 @@ packages: resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} engines: {node: '>= 12.13.0'} - regexp-to-ast@0.5.0: - resolution: {integrity: sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==} - - remeda@2.40.0: - resolution: {integrity: sha512-Y8yxrbK8SbTao2+mTqJcjre3/eBH641nr88aAgYfOdbW5p4FH+1Jk3mMypnA+SoktEXBVA8id0o3XPbgHYvQ9Q==} + remeda@2.45.0: + resolution: {integrity: sha512-CTftZ0GJSox3CH77OhJemj20sXXUScH/kpkExD6t2LP3IG+mPKASBMFkbvWtsvzhU+54gKcgH38WWVihfr7bCA==} engines: {node: '>=18.0.0'} require-from-string@2.0.2: @@ -5127,13 +5213,13 @@ packages: engines: {node: '>= 0.4'} hasBin: true - rolldown@1.2.5: - resolution: {integrity: sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==} + rolldown@1.2.6: + resolution: {integrity: sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rollup@4.62.4: - resolution: {integrity: sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==} + rollup@4.63.1: + resolution: {integrity: sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -5213,9 +5299,6 @@ packages: secure-json-parse@4.1.0: resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} - semifies@1.0.0: - resolution: {integrity: sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==} - semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -5228,8 +5311,8 @@ packages: set-cookie-parser@3.1.2: resolution: {integrity: sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==} - shadcn-svelte@1.5.0: - resolution: {integrity: sha512-fcxTeUwEIvELkL/h0Vg2i/9Sf6exAmLG5u3GXc0aN8CA+3fNffKQnfrTdMgkjUEmx9XP619I+wcJOvlr5aDWYQ==} + shadcn-svelte@1.6.1: + resolution: {integrity: sha512-qmbdBQ3GLO+kfLWlF7H+UZ0YbGOv4UnTy3r3LP6yK5QfO5SW+989ohKoAjKe+caHYWz7oFYqflVZablIewuBiw==} hasBin: true peerDependencies: svelte: ^5.0.0 @@ -5257,8 +5340,8 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - simple-git-hooks@2.13.1: - resolution: {integrity: sha512-WszCLXwT4h2k1ufIXAgsbiTOazqqevFCIncOuUBZJ91DdvWcC5+OFkluWRQPrcuSYd8fjq+o2y1QfWqYMoAToQ==} + simple-git-hooks@2.14.0: + resolution: {integrity: sha512-kkTORPAuxQz2g1QUuN0J8yGWT28tjGBfPOdJ4xT7Vbeu30veKUZQIoID2qGgCDAakaQn59UeZJJ4cFGB8PVP2A==} hasBin: true simple-wcswidth@1.1.2: @@ -5395,9 +5478,9 @@ packages: svelte: ^4.0.0 || ^5.0.0-next.0 typescript: ^5.0.0 || ^6.0.0 - svelte-eslint-parser@1.8.0: - resolution: {integrity: sha512-mikR1qwIVy3t5WthUoAXkMwxkXvabZP9FJgdx35Ei7EbGWmctva1Pih16Koeor/bdNNq8NXHlwKGS6NkYTawLg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0, pnpm: 10.34.1} + svelte-eslint-parser@1.8.1: + resolution: {integrity: sha512-5zgKBqAf6V8Jyrmr1jViksyG4NKT8NheYwkdxRaMRDAqpGWi9wR8ktcGrAZbfMn/PHUp1BWzAp0cYQECkWuAMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0, pnpm: 10.34.5} peerDependencies: svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 peerDependenciesMeta: @@ -5450,8 +5533,8 @@ packages: peerDependencies: svelte: ^5.30.2 - svelte@5.56.10: - resolution: {integrity: sha512-Lcxbj8I/KAbpY+VjtY4ENQBV0dDCipfGAhqb51XQZ67CIQqXgsv/8dPkbILaj4Fb6/b6JAEM/PIVbILXgDQy2g==} + svelte@5.57.0: + resolution: {integrity: sha512-NdbDn7fl4be1ViUG0oq/lvG6OZy3oENolV2ONjiqqsfVoeAfzaQAKUcEX3MrQod/Bebv1PgwET9rfXhgn9s4Kg==} engines: {node: '>=18'} sveltekit-superforms@2.30.2: @@ -5486,9 +5569,9 @@ packages: tailwindcss@4.3.3: resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} - takumi-js@2.10.0: - resolution: {integrity: sha512-SdaoNkuifjgJgWbsNBdm94mkgxw36iGeaUGFxsEk+HfkJBHW4LOCdaq4Bz9p7pQlVXlkxLj633uOWE4A1GRc2Q==} - engines: {node: '>=18'} + takumi-js@2.13.7: + resolution: {integrity: sha512-6CuATUv1O6X/aD3T0OQIkv0nyesefJibamskfQxcQqKVl2CCk6kIX9E0e+glMTILTJC2I1IcrcFEPlmDlcexgQ==} + engines: {node: '>=20.19'} tapable@2.3.3: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} @@ -5517,8 +5600,8 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} - tinypool@2.1.0: - resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} + tinypool@2.1.2: + resolution: {integrity: sha512-9YodfrxS9g9IbFr/KOjE5bAeJ0p61n3bW6mqvy0jtoeKd1kTW1Cxm0oulm6KX2lyM9Gl6WIe8nEbY7LWv5ZJww==} engines: {node: ^20.0.0 || >=22.0.0} tinyrainbow@3.1.1: @@ -5591,8 +5674,8 @@ packages: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} - typebox@1.3.14: - resolution: {integrity: sha512-uD3FHZb4St/7E7eY0YANSt+yZABDDlbeJbXhUU17x2GZLyHUiQ6+mxSpZhZtnJSjKy/7PZ49r7SDZHG4F3JsLw==} + typebox@1.3.22: + resolution: {integrity: sha512-YoX6QdtuJR/+YpP/qXf9gJXTxvSuiUlKYgW3AilsOSLlsZe/VH6ILR5vhoJM5QXY//uzIRE7NYij3h2+nG9vRQ==} typedoc-plugin-coverage@4.0.3: resolution: {integrity: sha512-baim3wyMkqpX7rBzL/6iZ7wzKJuSr9ffP16RHOsdTUNoHUZeXLIZHSUBtUhXmNHaUNRgfqdmKLBwyggbJjGdeQ==} @@ -5600,8 +5683,8 @@ packages: peerDependencies: typedoc: 0.28.x - typedoc-plugin-markdown@4.12.0: - resolution: {integrity: sha512-eJDEMAfxCmede22c/Jw7d0FA13ggAQv+KkwQYKYCdqI02cin6Rc9QRwbG/7XvvHWinuFejySnZVUWDtvGk3Vbg==} + typedoc-plugin-markdown@4.13.0: + resolution: {integrity: sha512-OHaOLoMTS0wL2ud73WXvxv486mrUEtgXxW4iKiTOGJVipT/VBWY76rlVRMQGes7aVonoIf5roWRe1UvHbufLlQ==} engines: {node: '>= 18'} peerDependencies: typedoc: 0.28.x @@ -5613,8 +5696,8 @@ packages: peerDependencies: typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x - typescript-eslint@8.67.0: - resolution: {integrity: sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==} + typescript-eslint@8.70.0: + resolution: {integrity: sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -5632,8 +5715,8 @@ packages: resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} engines: {node: '>=18'} - undici-types@8.3.0: - resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + undici-types@8.9.0: + resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} undici@7.29.0: resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} @@ -5717,13 +5800,13 @@ packages: vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} - vite@8.2.2: - resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} + vite@8.3.0: + resolution: {integrity: sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.4.0 || ^0.5.0 + '@vitejs/devtools': ^0.7.1 esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 @@ -5768,11 +5851,11 @@ packages: vite: optional: true - vitest-browser-svelte@3.0.0: - resolution: {integrity: sha512-BKM9iBGNsEQzigWuSZFAiw6AJDqQqMttApoD8gM8LTI6vxfd423rDMN8GqUHJyGbhH3XcOo9N9u/4hu47Nxhsw==} + vitest-browser-svelte@3.1.0: + resolution: {integrity: sha512-+EBXSed6NArmzOC2DY4v9sI5n9TMIyfkMnewbXNQ7sR5IckQTCx7g1BbLdkH7kzm/tkgG/eEbRYlMWSbVfBtWA==} peerDependencies: svelte: ^3 || ^4 || ^5 || ^5.0.0-next.0 - vitest: ^4.0.0 + vitest: ^4.0.0 || ^5.0.0 vitest@4.1.11: resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} @@ -5858,9 +5941,9 @@ packages: utf-8-validate: optional: true - wsl-utils@0.1.0: - resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} - engines: {node: '>=18'} + wsl-utils@1.0.0: + resolution: {integrity: sha512-Hl0ZOAs672vg+06kfujwRhoS6/jehvULrlFkuF2dRu6pHgA8U06h3xqNIqNNU1LTXPcedxByAR4GS6pwQK0mgA==} + engines: {node: '>=20'} xss@1.0.15: resolution: {integrity: sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==} @@ -5907,8 +5990,8 @@ packages: resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} engines: {node: '>=12.20'} - yocto-spinner@0.2.3: - resolution: {integrity: sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ==} + yocto-spinner@1.2.2: + resolution: {integrity: sha512-DODGl1wJjA/s5pnJFKau9lIYHT81lnhob1i3e1TjxZRxEhWRKl74nTbWE6H5KlkViQQTo/Z29YFdxzTZAMY3ng==} engines: {node: '>=18.19'} yoctocolors@2.2.0: @@ -5926,8 +6009,8 @@ packages: peerDependencies: zod: ^3.25 || ^4.0.14 - zod@4.4.3: - resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zod@4.6.2: + resolution: {integrity: sha512-lh5RCAGFa1Cm2hjtNwLQhSs/AsqdWnTQaBER9fEwN/88pSh7KOtJavtBx/0VlkN/uFd61SwYmljLMDAsHlvzBQ==} zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -5938,31 +6021,7 @@ snapshots: dependencies: '@jsdevtools/ono': 7.1.3 '@types/json-schema': 7.0.15 - js-yaml: 4.3.1 - - '@apm-js-collab/code-transformer-bundler-plugins@0.7.4': - dependencies: - '@apm-js-collab/code-transformer': 0.18.1 - es-module-lexer: 2.3.1 - magic-string: 0.30.21 - module-details-from-path: 1.0.4 - - '@apm-js-collab/code-transformer@0.18.1': - dependencies: - '@types/estree': 1.0.9 - astring: 1.9.0 - esquery: 1.7.0 - meriyah: 6.1.4 - semifies: 1.0.0 - source-map: 0.6.1 - - '@apm-js-collab/tracing-hooks@0.13.0(supports-color@7.2.0)': - dependencies: - '@apm-js-collab/code-transformer': 0.18.1 - debug: 4.4.3(supports-color@7.2.0) - module-details-from-path: 1.0.4 - transitivePeerDependencies: - - supports-color + js-yaml: 4.3.2 '@ark/schema@0.56.2': dependencies: @@ -6203,82 +6262,96 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@better-auth/api-key@1.6.29(04ce92fbd93057ce4052b8a6f33b15c6)': + '@better-auth/api-key@1.7.7(2876af4a9b015b644355db9021441558)': dependencies: - '@better-auth/core': 1.6.29(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.5)(nanostores@1.4.2) + '@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.2))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) '@better-auth/utils': 0.4.2 - better-auth: 1.6.29(@opentelemetry/api@1.9.1)(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(kysely@0.29.5)(postgres@3.4.9))(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vitest@4.1.11) - better-call: 1.4.0(zod@4.4.3) - zod: 4.4.3 + better-auth: 1.7.7(@opentelemetry/api@1.9.1)(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(drizzle-kit@0.31.11)(drizzle-orm@0.45.4(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(kysely@0.29.5)(postgres@3.4.9))(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.63.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vitest@4.1.11) + better-call: 1.4.0(zod@4.6.2) + zod: 4.6.2 - '@better-auth/core@1.6.29(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.5)(nanostores@1.4.2)': + '@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.2))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2)': dependencies: '@better-auth/utils': 0.4.2 - '@better-fetch/fetch': 1.3.1 + '@better-fetch/fetch': 1.3.2 '@opentelemetry/semantic-conventions': 1.43.0 '@standard-schema/spec': 1.1.0 - better-call: 1.4.0(zod@4.4.3) - jose: 6.2.8 + better-call: 1.4.0(zod@4.6.2) + jose: 6.2.10 kysely: 0.29.5 - nanostores: 1.4.2 - zod: 4.4.3 + nanostores: 1.5.2 + zod: 4.6.2 optionalDependencies: '@opentelemetry/api': 1.9.1 - '@better-auth/drizzle-adapter@1.6.29(@better-auth/core@1.6.29(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.5)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(kysely@0.29.5)(postgres@3.4.9))': + '@better-auth/drizzle-adapter@1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.2))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.4(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(kysely@0.29.5)(postgres@3.4.9))': dependencies: - '@better-auth/core': 1.6.29(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.5)(nanostores@1.4.2) + '@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.2))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) '@better-auth/utils': 0.4.2 optionalDependencies: - drizzle-orm: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(kysely@0.29.5)(postgres@3.4.9) + drizzle-orm: 0.45.4(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(kysely@0.29.5)(postgres@3.4.9) - '@better-auth/kysely-adapter@1.6.29(@better-auth/core@1.6.29(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.5)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(kysely@0.29.5)': + '@better-auth/kysely-adapter@1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.2))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(kysely@0.29.5)': dependencies: - '@better-auth/core': 1.6.29(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.5)(nanostores@1.4.2) + '@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.2))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) '@better-auth/utils': 0.4.2 optionalDependencies: kysely: 0.29.5 - '@better-auth/memory-adapter@1.6.29(@better-auth/core@1.6.29(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.5)(nanostores@1.4.2))(@better-auth/utils@0.4.2)': + '@better-auth/memory-adapter@1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.2))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)': dependencies: - '@better-auth/core': 1.6.29(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.5)(nanostores@1.4.2) + '@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.2))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) '@better-auth/utils': 0.4.2 - '@better-auth/mongo-adapter@1.6.29(@better-auth/core@1.6.29(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.5)(nanostores@1.4.2))(@better-auth/utils@0.4.2)': + '@better-auth/mongo-adapter@1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.2))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)': dependencies: - '@better-auth/core': 1.6.29(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.5)(nanostores@1.4.2) + '@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.2))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) '@better-auth/utils': 0.4.2 - '@better-auth/prisma-adapter@1.6.29(@better-auth/core@1.6.29(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.5)(nanostores@1.4.2))(@better-auth/utils@0.4.2)': + '@better-auth/prisma-adapter@1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.2))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)': dependencies: - '@better-auth/core': 1.6.29(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.5)(nanostores@1.4.2) + '@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.2))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) '@better-auth/utils': 0.4.2 - '@better-auth/telemetry@1.6.29(@better-auth/core@1.6.29(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.5)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)': + '@better-auth/telemetry@1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.2))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)': dependencies: - '@better-auth/core': 1.6.29(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.5)(nanostores@1.4.2) + '@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.2))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) '@better-auth/utils': 0.4.2 - '@better-fetch/fetch': 1.3.1 + '@better-fetch/fetch': 1.3.2 '@better-auth/utils@0.4.2': dependencies: - '@noble/hashes': 2.3.0 + '@noble/hashes': 2.4.0 '@better-auth/utils@0.5.0': dependencies: - '@noble/hashes': 2.3.0 + '@noble/hashes': 2.4.0 '@better-fetch/fetch@1.3.1': {} + '@better-fetch/fetch@1.3.2': {} + '@blazediff/core@1.9.1': {} '@borewit/text-codec@0.2.2': {} - '@changesets/apply-release-plan@8.0.0': + '@cacheable/memory@2.2.0': + dependencies: + '@cacheable/utils': 2.5.0 + '@keyv/bigmap': 1.3.1(keyv@5.6.0) + hookified: 1.15.1 + keyv: 5.6.0 + + '@cacheable/utils@2.5.0': + dependencies: + hashery: 1.5.1 + keyv: 5.6.0 + + '@changesets/apply-release-plan@8.1.0': dependencies: '@changesets/config': 4.0.0 '@changesets/format': 0.1.2 - '@changesets/git': 4.0.0 + '@changesets/git': 4.0.1 '@changesets/should-skip-package': 1.0.0 '@changesets/types': 7.0.0 import-meta-resolve: 4.2.0 @@ -6297,17 +6370,17 @@ snapshots: dependencies: '@changesets/types': 7.0.0 - '@changesets/cli@3.0.1': + '@changesets/cli@3.0.2': dependencies: - '@changesets/apply-release-plan': 8.0.0 + '@changesets/apply-release-plan': 8.1.0 '@changesets/assemble-release-plan': 7.0.0 '@changesets/changelog-git': 1.0.0 '@changesets/config': 4.0.0 '@changesets/errors': 1.0.0 '@changesets/get-dependents-graph': 3.0.0 - '@changesets/git': 4.0.0 + '@changesets/git': 4.0.1 '@changesets/pre': 3.0.0 - '@changesets/read': 1.0.0 + '@changesets/read': 1.0.1 '@changesets/should-skip-package': 1.0.0 '@changesets/types': 7.0.0 '@changesets/write': 1.0.1 @@ -6327,7 +6400,7 @@ snapshots: '@changesets/should-skip-package': 1.0.0 '@changesets/types': 7.0.0 '@manypkg/get-packages': 3.1.0 - picomatch: 4.0.5 + picomatch: 4.0.7 '@changesets/errors@1.0.0': {} @@ -6348,12 +6421,12 @@ snapshots: transitivePeerDependencies: - encoding - '@changesets/git@4.0.0': + '@changesets/git@4.0.1': dependencies: '@changesets/errors': 1.0.0 '@changesets/types': 7.0.0 '@manypkg/get-packages': 3.1.0 - picomatch: 4.0.5 + picomatch: 4.0.7 tinyexec: 1.3.0 '@changesets/parse@1.0.0': @@ -6367,9 +6440,9 @@ snapshots: '@changesets/types': 7.0.0 '@manypkg/get-packages': 3.1.0 - '@changesets/read@1.0.0': + '@changesets/read@1.0.1': dependencies: - '@changesets/git': 4.0.0 + '@changesets/git': 4.0.1 '@changesets/parse': 1.0.0 '@changesets/types': 7.0.0 @@ -6387,20 +6460,20 @@ snapshots: '@changesets/types': 7.0.0 human-id: 4.2.1 - '@chevrotain/cst-dts-gen@10.5.0': + '@chevrotain/cst-dts-gen@12.0.0': dependencies: - '@chevrotain/gast': 10.5.0 - '@chevrotain/types': 10.5.0 - lodash: 4.17.21 + '@chevrotain/gast': 12.0.0 + '@chevrotain/types': 12.0.0 - '@chevrotain/gast@10.5.0': + '@chevrotain/gast@12.0.0': dependencies: - '@chevrotain/types': 10.5.0 - lodash: 4.17.21 + '@chevrotain/types': 12.0.0 + + '@chevrotain/regexp-to-ast@12.0.0': {} - '@chevrotain/types@10.5.0': {} + '@chevrotain/types@12.0.0': {} - '@chevrotain/utils@10.5.0': {} + '@chevrotain/utils@12.0.0': {} '@clack/core@1.4.3': dependencies: @@ -6418,12 +6491,12 @@ snapshots: dependencies: commander: 15.0.0 - '@commitlint/cli@21.2.2(@types/node@26.2.0)(conventional-commits-parser@7.1.2)(typescript@6.0.3)': + '@commitlint/cli@21.2.2(@types/node@26.5.1)(conventional-commits-parser@7.1.2)(typescript@6.0.3)': dependencies: '@commitlint/config-conventional': 21.2.2 '@commitlint/format': 21.2.2 '@commitlint/lint': 21.2.2 - '@commitlint/load': 21.2.2(@types/node@26.2.0)(typescript@6.0.3) + '@commitlint/load': 21.2.2(@types/node@26.5.1)(typescript@6.0.3) '@commitlint/read': 21.2.1(conventional-commits-parser@7.1.2) '@commitlint/types': 21.2.0 tinyexec: 1.3.0 @@ -6437,7 +6510,7 @@ snapshots: '@commitlint/config-conventional@21.2.2': dependencies: '@commitlint/types': 21.2.0 - conventional-changelog-conventionalcommits: 10.3.0 + conventional-changelog-conventionalcommits: 10.4.0 '@commitlint/config-validator@21.2.0': dependencies: @@ -6447,7 +6520,7 @@ snapshots: '@commitlint/ensure@21.2.0': dependencies: '@commitlint/types': 21.2.0 - es-toolkit: 1.50.0 + es-toolkit: 1.52.0 '@commitlint/execute-rule@21.0.1': {} @@ -6468,15 +6541,15 @@ snapshots: '@commitlint/rules': 21.2.2 '@commitlint/types': 21.2.0 - '@commitlint/load@21.2.2(@types/node@26.2.0)(typescript@6.0.3)': + '@commitlint/load@21.2.2(@types/node@26.5.1)(typescript@6.0.3)': dependencies: '@commitlint/config-validator': 21.2.0 '@commitlint/execute-rule': 21.0.1 '@commitlint/resolve-extends': 21.2.2 '@commitlint/types': 21.2.0 cosmiconfig: 9.0.2(typescript@6.0.3) - cosmiconfig-typescript-loader: 6.3.0(@types/node@26.2.0)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3) - es-toolkit: 1.50.0 + cosmiconfig-typescript-loader: 6.3.0(@types/node@26.5.1)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3) + es-toolkit: 1.52.0 is-plain-obj: 4.1.0 picocolors: 1.1.1 transitivePeerDependencies: @@ -6488,7 +6561,7 @@ snapshots: '@commitlint/parse@21.2.2': dependencies: '@commitlint/types': 21.2.0 - conventional-changelog-angular: 9.3.0 + conventional-changelog-angular: 9.4.0 conventional-commits-parser: 7.1.2 '@commitlint/read@21.2.1(conventional-commits-parser@7.1.2)': @@ -6505,7 +6578,7 @@ snapshots: dependencies: '@commitlint/config-validator': 21.2.0 '@commitlint/types': 21.2.0 - es-toolkit: 1.50.0 + es-toolkit: 1.52.0 global-directory: 5.0.0 resolve-from: 5.0.0 @@ -6535,7 +6608,7 @@ snapshots: optionalDependencies: conventional-commits-parser: 7.1.2 - '@conventional-changelog/template@1.3.0': {} + '@conventional-changelog/template@1.4.0': {} '@date-fns/tz@1.2.0': {} @@ -6603,12 +6676,12 @@ snapshots: '@preact/signals-core': 1.14.4 tslib: 2.8.1 - '@dnd-kit/svelte@0.5.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))': + '@dnd-kit/svelte@0.5.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))': dependencies: '@dnd-kit/abstract': 0.5.0 '@dnd-kit/dom': 0.5.0 '@dnd-kit/state': 0.5.0 - svelte: 5.56.10(@typescript-eslint/types@8.67.0) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) tslib: 2.8.1 '@dnd-kit/utilities@3.2.2(react@19.2.7)': @@ -6623,6 +6696,8 @@ snapshots: tslib: 2.8.1 optional: true + '@emoji-mart/data@1.2.1': {} + '@emotion/babel-plugin@11.13.5(supports-color@7.2.0)': dependencies: '@babel/helper-module-imports': 7.29.7(supports-color@7.2.0) @@ -6695,7 +6770,7 @@ snapshots: '@esbuild-kit/esm-loader@2.6.5': dependencies: '@esbuild-kit/core-utils': 3.3.2 - get-tsconfig: 4.14.2 + get-tsconfig: 4.14.3 '@esbuild/aix-ppc64@0.25.12': optional: true @@ -6919,9 +6994,9 @@ snapshots: '@esbuild/win32-x64@0.28.2': optional: true - '@eslint-community/eslint-utils@4.10.1(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))': + '@eslint-community/eslint-utils@4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))': dependencies: - eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + eslint: 10.10.0(jiti@2.7.0)(supports-color@7.2.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -6942,13 +7017,13 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))': + '@eslint/js@10.0.1(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))': optionalDependencies: - eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + eslint: 10.10.0(jiti@2.7.0)(supports-color@7.2.0) '@eslint/object-schema@3.0.5': {} - '@eslint/plugin-kit@0.7.2': + '@eslint/plugin-kit@0.7.3': dependencies: '@eslint/core': 1.2.1 levn: 0.4.1 @@ -7134,7 +7209,7 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true - '@internationalized/date@3.12.3': + '@internationalized/date@3.12.4': dependencies: '@swc/helpers': 0.5.23 @@ -7152,13 +7227,23 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} + '@jridgewell/sourcemap-codec@1.6.0': {} + '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 '@jsdevtools/ono@7.1.3': {} + '@keyv/bigmap@1.3.1(keyv@5.6.0)': + dependencies: + hashery: 1.5.1 + hookified: 1.15.1 + keyv: 5.6.0 + + '@keyv/serialize@1.1.1': {} + '@lexical/clipboard@0.41.0': dependencies: '@lexical/html': 0.41.0 @@ -7203,7 +7288,7 @@ snapshots: '@lexical/headless@0.41.0': dependencies: - happy-dom: 20.11.2 + happy-dom: 20.12.0 lexical: 0.41.0 transitivePeerDependencies: - bufferutil @@ -7265,7 +7350,7 @@ snapshots: '@lexical/utils': 0.41.0 lexical: 0.41.0 - '@lexical/react@0.41.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(yjs@13.6.31)': + '@lexical/react@0.41.0(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(yjs@13.6.31)': dependencies: '@floating-ui/react': 0.27.20(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@lexical/devtools-core': 0.41.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -7287,8 +7372,9 @@ snapshots: lexical: 0.41.0 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - react-error-boundary: 6.1.2(react@19.2.7) + react-error-boundary: 6.1.3(@types/react@19.2.16)(react@19.2.7) transitivePeerDependencies: + - '@types/react' - yjs '@lexical/rich-text@0.41.0': @@ -7326,9 +7412,9 @@ snapshots: lexical: 0.41.0 yjs: 13.6.31 - '@lucide/svelte@1.33.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))': + '@lucide/svelte@1.54.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))': dependencies: - svelte: 5.56.10(@typescript-eslint/types@8.67.0) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) '@manypkg/find-root@3.1.0': dependencies: @@ -7356,9 +7442,9 @@ snapshots: react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - '@mrleebo/prisma-ast@0.13.1': + '@mrleebo/prisma-ast@0.16.0': dependencies: - chevrotain: 10.5.0 + chevrotain: 12.0.0 lilconfig: 2.1.0 '@napi-rs/lzma-linux-x64-gnu@1.5.1': @@ -7392,9 +7478,9 @@ snapshots: '@next/swc-win32-x64-msvc@16.2.7': optional: true - '@noble/ciphers@2.3.0': {} + '@noble/ciphers@2.4.0': {} - '@noble/hashes@2.3.0': {} + '@noble/hashes@2.4.0': {} '@opentelemetry/api-logs@0.220.0': dependencies: @@ -7439,23 +7525,23 @@ snapshots: '@opentelemetry/semantic-conventions@1.43.0': {} - '@orval/angular@8.24.0(supports-color@7.2.0)(typescript@6.0.3)': + '@orval/angular@8.31.0(supports-color@7.2.0)(typescript@6.0.3)': dependencies: - '@orval/core': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) + '@orval/core': 8.31.0(supports-color@7.2.0)(typescript@6.0.3) transitivePeerDependencies: - '@faker-js/faker' - supports-color - typescript - '@orval/axios@8.24.0(supports-color@7.2.0)(typescript@6.0.3)': + '@orval/axios@8.31.0(supports-color@7.2.0)(typescript@6.0.3)': dependencies: - '@orval/core': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) + '@orval/core': 8.31.0(supports-color@7.2.0)(typescript@6.0.3) transitivePeerDependencies: - '@faker-js/faker' - supports-color - typescript - '@orval/core@8.24.0(supports-color@7.2.0)(typescript@6.0.3)': + '@orval/core@8.31.0(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@scalar/openapi-types': 0.9.4 acorn: 8.18.0 @@ -7466,92 +7552,97 @@ snapshots: fs-extra: 11.4.0 jiti: 2.7.0 jsesc: 3.1.0 - remeda: 2.40.0 + remeda: 2.45.0 tinyglobby: 0.2.17 typedoc: 0.28.20(typescript@6.0.3) transitivePeerDependencies: - supports-color - typescript - '@orval/effect@8.24.0(supports-color@7.2.0)(typescript@6.0.3)': + '@orval/effect@8.31.0(effect@3.22.1)(supports-color@7.2.0)(typescript@6.0.3)': dependencies: - '@orval/core': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) - remeda: 2.40.0 + '@orval/core': 8.31.0(supports-color@7.2.0)(typescript@6.0.3) + remeda: 2.45.0 + optionalDependencies: + effect: 3.22.1 transitivePeerDependencies: - '@faker-js/faker' - supports-color - typescript - '@orval/fetch@8.24.0(supports-color@7.2.0)(typescript@6.0.3)': + '@orval/fetch@8.31.0(supports-color@7.2.0)(typescript@6.0.3)': dependencies: - '@orval/core': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) + '@orval/core': 8.31.0(supports-color@7.2.0)(typescript@6.0.3) transitivePeerDependencies: - '@faker-js/faker' - supports-color - typescript - '@orval/hono@8.24.0(supports-color@7.2.0)(typescript@6.0.3)': + '@orval/hono@8.31.0(supports-color@7.2.0)(typescript@6.0.3)(zod@4.6.2)': dependencies: - '@orval/core': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) - '@orval/zod': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) + '@orval/core': 8.31.0(supports-color@7.2.0)(typescript@6.0.3) + '@orval/zod': 8.31.0(supports-color@7.2.0)(typescript@6.0.3)(zod@4.6.2) fs-extra: 11.4.0 optionalDependencies: typescript: 6.0.3 transitivePeerDependencies: - '@faker-js/faker' - supports-color + - zod - '@orval/mcp@8.24.0(supports-color@7.2.0)(typescript@6.0.3)': + '@orval/mcp@8.31.0(supports-color@7.2.0)(typescript@6.0.3)(zod@4.6.2)': dependencies: - '@orval/core': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) - '@orval/fetch': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) - '@orval/zod': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) + '@orval/core': 8.31.0(supports-color@7.2.0)(typescript@6.0.3) + '@orval/fetch': 8.31.0(supports-color@7.2.0)(typescript@6.0.3) + '@orval/zod': 8.31.0(supports-color@7.2.0)(typescript@6.0.3)(zod@4.6.2) transitivePeerDependencies: - '@faker-js/faker' - supports-color - typescript + - zod - '@orval/mock@8.24.0(supports-color@7.2.0)(typescript@6.0.3)': + '@orval/mock@8.31.0(supports-color@7.2.0)(typescript@6.0.3)': dependencies: - '@orval/core': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) - remeda: 2.40.0 + '@orval/core': 8.31.0(supports-color@7.2.0)(typescript@6.0.3) + remeda: 2.45.0 transitivePeerDependencies: - - '@faker-js/faker' - supports-color - typescript - '@orval/query@8.24.0(supports-color@7.2.0)(typescript@6.0.3)': + '@orval/query@8.31.0(supports-color@7.2.0)(typescript@6.0.3)': dependencies: - '@orval/core': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) - '@orval/fetch': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) - remeda: 2.40.0 + '@orval/core': 8.31.0(supports-color@7.2.0)(typescript@6.0.3) + '@orval/fetch': 8.31.0(supports-color@7.2.0)(typescript@6.0.3) + remeda: 2.45.0 transitivePeerDependencies: - '@faker-js/faker' - supports-color - typescript - '@orval/solid-start@8.24.0(supports-color@7.2.0)(typescript@6.0.3)': + '@orval/solid-start@8.31.0(supports-color@7.2.0)(typescript@6.0.3)': dependencies: - '@orval/core': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) + '@orval/core': 8.31.0(supports-color@7.2.0)(typescript@6.0.3) transitivePeerDependencies: - '@faker-js/faker' - supports-color - typescript - '@orval/swr@8.24.0(supports-color@7.2.0)(typescript@6.0.3)': + '@orval/swr@8.31.0(supports-color@7.2.0)(typescript@6.0.3)': dependencies: - '@orval/core': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) - '@orval/fetch': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) + '@orval/core': 8.31.0(supports-color@7.2.0)(typescript@6.0.3) + '@orval/fetch': 8.31.0(supports-color@7.2.0)(typescript@6.0.3) transitivePeerDependencies: - '@faker-js/faker' - supports-color - typescript - '@orval/zod@8.24.0(supports-color@7.2.0)(typescript@6.0.3)': + '@orval/zod@8.31.0(supports-color@7.2.0)(typescript@6.0.3)(zod@4.6.2)': dependencies: - '@orval/core': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) + '@orval/core': 8.31.0(supports-color@7.2.0)(typescript@6.0.3) jsesc: 3.1.0 - remeda: 2.40.0 + remeda: 2.45.0 + optionalDependencies: + zod: 4.6.2 transitivePeerDependencies: - '@faker-js/faker' - supports-color @@ -7559,84 +7650,84 @@ snapshots: '@oslojs/encoding@1.1.0': {} - '@oxc-project/types@0.146.0': {} + '@oxc-project/types@0.147.0': {} - '@oxfmt/binding-android-arm-eabi@0.64.0': + '@oxfmt/binding-android-arm-eabi@0.67.0': optional: true - '@oxfmt/binding-android-arm64@0.64.0': + '@oxfmt/binding-android-arm64@0.67.0': optional: true - '@oxfmt/binding-darwin-arm64@0.64.0': + '@oxfmt/binding-darwin-arm64@0.67.0': optional: true - '@oxfmt/binding-darwin-x64@0.64.0': + '@oxfmt/binding-darwin-x64@0.67.0': optional: true - '@oxfmt/binding-freebsd-x64@0.64.0': + '@oxfmt/binding-freebsd-x64@0.67.0': optional: true - '@oxfmt/binding-linux-arm-gnueabihf@0.64.0': + '@oxfmt/binding-linux-arm-gnueabihf@0.67.0': optional: true - '@oxfmt/binding-linux-arm-musleabihf@0.64.0': + '@oxfmt/binding-linux-arm-musleabihf@0.67.0': optional: true - '@oxfmt/binding-linux-arm64-gnu@0.64.0': + '@oxfmt/binding-linux-arm64-gnu@0.67.0': optional: true - '@oxfmt/binding-linux-arm64-musl@0.64.0': + '@oxfmt/binding-linux-arm64-musl@0.67.0': optional: true - '@oxfmt/binding-linux-ppc64-gnu@0.64.0': + '@oxfmt/binding-linux-ppc64-gnu@0.67.0': optional: true - '@oxfmt/binding-linux-riscv64-gnu@0.64.0': + '@oxfmt/binding-linux-riscv64-gnu@0.67.0': optional: true - '@oxfmt/binding-linux-riscv64-musl@0.64.0': + '@oxfmt/binding-linux-riscv64-musl@0.67.0': optional: true - '@oxfmt/binding-linux-s390x-gnu@0.64.0': + '@oxfmt/binding-linux-s390x-gnu@0.67.0': optional: true - '@oxfmt/binding-linux-x64-gnu@0.64.0': + '@oxfmt/binding-linux-x64-gnu@0.67.0': optional: true - '@oxfmt/binding-linux-x64-musl@0.64.0': + '@oxfmt/binding-linux-x64-musl@0.67.0': optional: true - '@oxfmt/binding-openharmony-arm64@0.64.0': + '@oxfmt/binding-openharmony-arm64@0.67.0': optional: true - '@oxfmt/binding-win32-arm64-msvc@0.64.0': + '@oxfmt/binding-win32-arm64-msvc@0.67.0': optional: true - '@oxfmt/binding-win32-ia32-msvc@0.64.0': + '@oxfmt/binding-win32-ia32-msvc@0.67.0': optional: true - '@oxfmt/binding-win32-x64-msvc@0.64.0': + '@oxfmt/binding-win32-x64-msvc@0.67.0': optional: true - '@payloadcms/graphql@3.88.0(graphql@16.14.1)(payload@3.88.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3))(typescript@6.0.3)': + '@payloadcms/graphql@3.89.0(graphql@16.14.1)(payload@3.89.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3))(typescript@6.0.3)': dependencies: graphql: 16.14.1 graphql-scalars: 1.22.2(graphql@16.14.1) - payload: 3.88.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3) + payload: 3.89.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3) pluralize: 8.0.0 ts-essentials: 10.0.3(typescript@6.0.3) tsx: 4.22.4 transitivePeerDependencies: - typescript - '@payloadcms/next@3.88.0(@types/react@19.2.16)(graphql@16.14.1)(monaco-editor@0.55.1)(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(payload@3.88.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@7.2.0)(typescript@6.0.3)': + '@payloadcms/next@3.89.0(@types/react@19.2.16)(graphql@16.14.1)(monaco-editor@0.55.1)(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.63.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(payload@3.89.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@dnd-kit/core': 6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@dnd-kit/modifiers': 9.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) '@dnd-kit/sortable': 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) - '@payloadcms/graphql': 3.88.0(graphql@16.14.1)(payload@3.88.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3))(typescript@6.0.3) - '@payloadcms/translations': 3.88.0 - '@payloadcms/ui': 3.88.0(@types/react@19.2.16)(monaco-editor@0.55.1)(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(payload@3.88.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@7.2.0)(typescript@6.0.3) + '@payloadcms/graphql': 3.89.0(graphql@16.14.1)(payload@3.89.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3))(typescript@6.0.3) + '@payloadcms/translations': 3.89.0 + '@payloadcms/ui': 3.89.0(@types/react@19.2.16)(monaco-editor@0.55.1)(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.63.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(payload@3.89.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@7.2.0)(typescript@6.0.3) busboy: 1.6.0 dequal: 2.0.3 file-type: 21.3.4(supports-color@7.2.0) @@ -7644,9 +7735,9 @@ snapshots: graphql-http: 1.22.4(graphql@16.14.1) graphql-playground-html: 1.6.30 http-status: 2.1.0 - next: 16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4) + next: 16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.63.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4) path-to-regexp: 6.3.0 - payload: 3.88.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3) + payload: 3.89.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3) qs-esm: 8.0.1 sass: 1.77.4 uuid: 13.0.2 @@ -7658,7 +7749,7 @@ snapshots: - supports-color - typescript - ? '@payloadcms/richtext-lexical@3.88.0(@faceless-ui/modal@3.0.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@faceless-ui/scroll-info@2.0.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@payloadcms/next@3.88.0(@types/react@19.2.16)(graphql@16.14.1)(monaco-editor@0.55.1)(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(payload@3.88.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@7.2.0)(typescript@6.0.3))(@types/react@19.2.16)(monaco-editor@0.55.1)(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(payload@3.88.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@7.2.0)(typescript@6.0.3)(yjs@13.6.31)' + ? '@payloadcms/richtext-lexical@3.89.0(@faceless-ui/modal@3.0.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@faceless-ui/scroll-info@2.0.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@payloadcms/next@3.89.0(@types/react@19.2.16)(graphql@16.14.1)(monaco-editor@0.55.1)(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.63.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(payload@3.89.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@7.2.0)(typescript@6.0.3))(@types/react@19.2.16)(monaco-editor@0.55.1)(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.63.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(payload@3.89.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@7.2.0)(typescript@6.0.3)(yjs@13.6.31)' : dependencies: '@faceless-ui/modal': 3.0.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@faceless-ui/scroll-info': 2.0.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -7668,14 +7759,14 @@ snapshots: '@lexical/link': 0.41.0 '@lexical/list': 0.41.0 '@lexical/mark': 0.41.0 - '@lexical/react': 0.41.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(yjs@13.6.31) + '@lexical/react': 0.41.0(@types/react@19.2.16)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(yjs@13.6.31) '@lexical/rich-text': 0.41.0 '@lexical/selection': 0.41.0 '@lexical/table': 0.41.0 '@lexical/utils': 0.41.0 - '@payloadcms/next': 3.88.0(@types/react@19.2.16)(graphql@16.14.1)(monaco-editor@0.55.1)(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(payload@3.88.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@7.2.0)(typescript@6.0.3) - '@payloadcms/translations': 3.88.0 - '@payloadcms/ui': 3.88.0(@types/react@19.2.16)(monaco-editor@0.55.1)(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(payload@3.88.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@7.2.0)(typescript@6.0.3) + '@payloadcms/next': 3.89.0(@types/react@19.2.16)(graphql@16.14.1)(monaco-editor@0.55.1)(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.63.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(payload@3.89.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@7.2.0)(typescript@6.0.3) + '@payloadcms/translations': 3.89.0 + '@payloadcms/ui': 3.89.0(@types/react@19.2.16)(monaco-editor@0.55.1)(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.63.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(payload@3.89.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@7.2.0)(typescript@6.0.3) acorn: 8.16.0 bson-objectid: 2.0.4 csstype: 3.1.3 @@ -7686,7 +7777,7 @@ snapshots: mdast-util-from-markdown: 2.0.2(supports-color@7.2.0) mdast-util-mdx-jsx: 3.1.3(supports-color@7.2.0) micromark-extension-mdx-jsx: 3.0.1 - payload: 3.88.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3) + payload: 3.89.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3) qs-esm: 8.0.1 react: 19.2.7 react-dom: 19.2.7(react@19.2.7) @@ -7703,11 +7794,11 @@ snapshots: - utf-8-validate - yjs - '@payloadcms/translations@3.88.0': + '@payloadcms/translations@3.89.0': dependencies: date-fns: 4.1.0 - '@payloadcms/ui@3.88.0(@types/react@19.2.16)(monaco-editor@0.55.1)(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(payload@3.88.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@7.2.0)(typescript@6.0.3)': + '@payloadcms/ui@3.89.0(@types/react@19.2.16)(monaco-editor@0.55.1)(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.63.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(payload@3.89.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@date-fns/tz': 1.2.0 '@dnd-kit/core': 6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -7717,14 +7808,15 @@ snapshots: '@faceless-ui/scroll-info': 2.0.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@faceless-ui/window-info': 3.0.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7) '@monaco-editor/react': 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@payloadcms/translations': 3.88.0 + '@payloadcms/translations': 3.89.0 + '@swc/helpers': 0.5.23 bson-objectid: 2.0.4 date-fns: 4.1.0 dequal: 2.0.3 md5: 2.3.0 - next: 16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4) + next: 16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.63.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4) object-to-formdata: 4.5.1 - payload: 3.88.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3) + payload: 3.89.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3) qs-esm: 8.0.1 react: 19.2.7 react-datepicker: 7.6.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -7744,9 +7836,9 @@ snapshots: '@pinojs/redact@0.4.0': {} - '@playwright/test@1.62.1': + '@playwright/test@1.63.0': dependencies: - playwright: 1.62.1 + playwright: 1.63.0 '@pnpm/deps.graph-sequencer@1100.0.1': {} @@ -7757,235 +7849,239 @@ snapshots: '@preact/signals-core@1.14.4': {} - '@rolldown/binding-android-arm-eabi@1.2.5': + '@rolldown/binding-android-arm-eabi@1.2.6': optional: true - '@rolldown/binding-android-arm64@1.2.5': + '@rolldown/binding-android-arm64@1.2.6': optional: true - '@rolldown/binding-darwin-arm64@1.2.5': + '@rolldown/binding-darwin-arm64@1.2.6': optional: true - '@rolldown/binding-darwin-x64@1.2.5': + '@rolldown/binding-darwin-x64@1.2.6': optional: true - '@rolldown/binding-freebsd-x64@1.2.5': + '@rolldown/binding-freebsd-x64@1.2.6': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.2.5': + '@rolldown/binding-linux-arm-gnueabihf@1.2.6': optional: true - '@rolldown/binding-linux-arm64-gnu@1.2.5': + '@rolldown/binding-linux-arm64-gnu@1.2.6': optional: true - '@rolldown/binding-linux-arm64-musl@1.2.5': + '@rolldown/binding-linux-arm64-musl@1.2.6': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.2.5': + '@rolldown/binding-linux-ppc64-gnu@1.2.6': optional: true - '@rolldown/binding-linux-s390x-gnu@1.2.5': + '@rolldown/binding-linux-s390x-gnu@1.2.6': optional: true - '@rolldown/binding-linux-x64-gnu@1.2.5': + '@rolldown/binding-linux-x64-gnu@1.2.6': optional: true - '@rolldown/binding-linux-x64-musl@1.2.5': + '@rolldown/binding-linux-x64-musl@1.2.6': optional: true - '@rolldown/binding-openharmony-arm64@1.2.5': + '@rolldown/binding-openharmony-arm64@1.2.6': optional: true - '@rolldown/binding-win32-arm64-msvc@1.2.5': + '@rolldown/binding-win32-arm64-msvc@1.2.6': optional: true - '@rolldown/binding-win32-x64-msvc@1.2.5': + '@rolldown/binding-win32-x64-msvc@1.2.6': optional: true '@rolldown/pluginutils@1.0.1': {} - '@rollup/plugin-commonjs@29.0.3(rollup@4.62.4)': + '@rollup/plugin-commonjs@29.0.3(rollup@4.63.1)': dependencies: - '@rollup/pluginutils': 5.4.0(rollup@4.62.4) + '@rollup/pluginutils': 5.4.0(rollup@4.63.1) commondir: 1.0.1 estree-walker: 2.0.2 - fdir: 6.5.0(picomatch@4.0.5) + fdir: 6.5.0(picomatch@4.0.7) is-reference: 1.2.1 magic-string: 0.30.21 - picomatch: 4.0.5 + picomatch: 4.0.7 optionalDependencies: - rollup: 4.62.4 + rollup: 4.63.1 - '@rollup/plugin-json@6.1.0(rollup@4.62.4)': + '@rollup/plugin-json@6.1.0(rollup@4.63.1)': dependencies: - '@rollup/pluginutils': 5.4.0(rollup@4.62.4) + '@rollup/pluginutils': 5.4.0(rollup@4.63.1) optionalDependencies: - rollup: 4.62.4 + rollup: 4.63.1 - '@rollup/plugin-node-resolve@16.0.3(rollup@4.62.4)': + '@rollup/plugin-node-resolve@16.0.3(rollup@4.63.1)': dependencies: - '@rollup/pluginutils': 5.4.0(rollup@4.62.4) + '@rollup/pluginutils': 5.4.0(rollup@4.63.1) '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-module: 1.0.0 resolve: 1.22.12 optionalDependencies: - rollup: 4.62.4 + rollup: 4.63.1 - '@rollup/plugin-replace@6.0.3(rollup@4.62.4)': + '@rollup/plugin-replace@6.0.3(rollup@4.63.1)': dependencies: - '@rollup/pluginutils': 5.4.0(rollup@4.62.4) + '@rollup/pluginutils': 5.4.0(rollup@4.63.1) magic-string: 0.30.21 optionalDependencies: - rollup: 4.62.4 + rollup: 4.63.1 - '@rollup/pluginutils@5.4.0(rollup@4.62.4)': + '@rollup/pluginutils@5.4.0(rollup@4.63.1)': dependencies: '@types/estree': 1.0.9 estree-walker: 2.0.2 - picomatch: 4.0.5 + picomatch: 4.0.7 optionalDependencies: - rollup: 4.62.4 + rollup: 4.63.1 - '@rollup/rollup-android-arm-eabi@4.62.4': + '@rollup/rollup-android-arm-eabi@4.63.1': optional: true - '@rollup/rollup-android-arm64@4.62.4': + '@rollup/rollup-android-arm64@4.63.1': optional: true - '@rollup/rollup-darwin-arm64@4.62.4': + '@rollup/rollup-darwin-arm64@4.63.1': optional: true - '@rollup/rollup-darwin-x64@4.62.4': + '@rollup/rollup-darwin-x64@4.63.1': optional: true - '@rollup/rollup-freebsd-arm64@4.62.4': + '@rollup/rollup-freebsd-arm64@4.63.1': optional: true - '@rollup/rollup-freebsd-x64@4.62.4': + '@rollup/rollup-freebsd-x64@4.63.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.62.4': + '@rollup/rollup-linux-arm-gnueabihf@4.63.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.62.4': + '@rollup/rollup-linux-arm-musleabihf@4.63.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.62.4': + '@rollup/rollup-linux-arm64-gnu@4.63.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.62.4': + '@rollup/rollup-linux-arm64-musl@4.63.1': optional: true - '@rollup/rollup-linux-loong64-gnu@4.62.4': + '@rollup/rollup-linux-loong64-gnu@4.63.1': optional: true - '@rollup/rollup-linux-loong64-musl@4.62.4': + '@rollup/rollup-linux-loong64-musl@4.63.1': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.62.4': + '@rollup/rollup-linux-ppc64-gnu@4.63.1': optional: true - '@rollup/rollup-linux-ppc64-musl@4.62.4': + '@rollup/rollup-linux-ppc64-musl@4.63.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.62.4': + '@rollup/rollup-linux-riscv64-gnu@4.63.1': optional: true - '@rollup/rollup-linux-riscv64-musl@4.62.4': + '@rollup/rollup-linux-riscv64-musl@4.63.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.62.4': + '@rollup/rollup-linux-s390x-gnu@4.63.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.62.4': + '@rollup/rollup-linux-x64-gnu@4.63.1': optional: true - '@rollup/rollup-linux-x64-musl@4.62.4': + '@rollup/rollup-linux-x64-musl@4.63.1': optional: true - '@rollup/rollup-openbsd-x64@4.62.4': + '@rollup/rollup-openbsd-x64@4.63.1': optional: true - '@rollup/rollup-openharmony-arm64@4.62.4': + '@rollup/rollup-openharmony-arm64@4.63.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.62.4': + '@rollup/rollup-win32-arm64-msvc@4.63.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.62.4': + '@rollup/rollup-win32-ia32-msvc@4.63.1': optional: true - '@rollup/rollup-win32-x64-gnu@4.62.4': + '@rollup/rollup-win32-x64-gnu@4.63.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.62.4': + '@rollup/rollup-win32-x64-msvc@4.63.1': optional: true '@scalar/helpers@0.10.0': {} + '@scalar/helpers@0.11.1': {} + '@scalar/json-magic@0.12.20': dependencies: '@scalar/helpers': 0.10.0 pathe: 2.0.3 yaml: 2.9.0 - '@scalar/json-magic@0.13.0': + '@scalar/json-magic@0.13.2': dependencies: - '@scalar/helpers': 0.10.0 + '@scalar/helpers': 0.11.1 pathe: 2.0.3 yaml: 2.9.0 - '@scalar/openapi-parser@0.28.14': + '@scalar/openapi-parser@0.28.16': dependencies: - '@scalar/helpers': 0.10.0 - '@scalar/json-magic': 0.13.0 - '@scalar/openapi-types': 0.9.4 - '@scalar/openapi-upgrader': 0.2.13 + '@scalar/helpers': 0.11.1 + '@scalar/json-magic': 0.13.2 + '@scalar/openapi-types': 0.9.5 + '@scalar/openapi-upgrader': 0.2.15 ajv: 8.20.0 ajv-draft-04: 1.0.0(ajv@8.20.0) - ajv-formats: 3.0.1(ajv@8.20.0) + ajv-formats: 3.0.1 jsonpointer: 5.0.1 leven: 4.1.0 yaml: 2.9.0 '@scalar/openapi-types@0.9.4': {} - '@scalar/openapi-upgrader@0.2.13': + '@scalar/openapi-types@0.9.5': {} + + '@scalar/openapi-upgrader@0.2.15': dependencies: - '@scalar/openapi-types': 0.9.4 + '@scalar/openapi-types': 0.9.5 '@sec-ant/readable-stream@0.4.1': {} - '@selemondev/svgl-svelte@2.17.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))': + '@selemondev/svgl-svelte@2.17.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))': dependencies: - svelte: 5.56.10(@typescript-eslint/types@8.67.0) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) - '@sentry/browser-utils@10.70.0': + '@sentry/browser-utils@10.74.0': dependencies: '@sentry/conventions': 0.16.0 - '@sentry/core': 10.70.0 + '@sentry/core': 10.74.0 - '@sentry/browser@10.70.0': + '@sentry/browser@10.74.0': dependencies: - '@sentry/browser-utils': 10.70.0 + '@sentry/browser-utils': 10.74.0 '@sentry/conventions': 0.16.0 - '@sentry/core': 10.70.0 - '@sentry/feedback': 10.70.0 - '@sentry/replay': 10.70.0 - '@sentry/replay-canvas': 10.70.0 + '@sentry/core': 10.74.0 + '@sentry/feedback': 10.74.0 + '@sentry/replay': 10.74.0 + '@sentry/replay-canvas': 10.74.0 - '@sentry/bundler-plugins@10.70.0(rollup@4.62.4)(supports-color@7.2.0)': + '@sentry/bundler-plugins@10.72.0(rollup@4.63.1)(supports-color@7.2.0)': dependencies: '@babel/core': 7.29.7(supports-color@7.2.0) '@sentry/cli': 2.58.6(supports-color@7.2.0) - '@sentry/core': 10.70.0 + '@sentry/core': 10.72.0 dotenv: 17.4.2 find-up: 5.0.0 glob: 13.0.6 magic-string: 0.30.21 optionalDependencies: - rollup: 4.62.4 + rollup: 4.63.1 transitivePeerDependencies: - encoding - supports-color @@ -8034,30 +8130,32 @@ snapshots: - encoding - supports-color - '@sentry/cloudflare@10.70.0(supports-color@7.2.0)': + '@sentry/cloudflare@10.74.0': dependencies: '@opentelemetry/api': 1.9.1 - '@sentry/core': 10.70.0 - '@sentry/server-utils': 10.70.0(supports-color@7.2.0) + '@sentry/core': 10.74.0 + '@sentry/server-utils': 10.74.0 magic-string: 0.30.21 - transitivePeerDependencies: - - supports-color '@sentry/conventions@0.16.0': {} - '@sentry/core@10.70.0': + '@sentry/core@10.72.0': dependencies: '@sentry/conventions': 0.16.0 - '@sentry/feedback@10.70.0': + '@sentry/core@10.74.0': dependencies: - '@sentry/core': 10.70.0 + '@sentry/conventions': 0.16.0 - '@sentry/node-core@10.70.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1)(supports-color@7.2.0))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))': + '@sentry/feedback@10.74.0': + dependencies: + '@sentry/core': 10.74.0 + + '@sentry/node-core@10.74.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1)(supports-color@7.2.0))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))': dependencies: '@sentry/conventions': 0.16.0 - '@sentry/core': 10.70.0 - '@sentry/opentelemetry': 10.70.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) + '@sentry/core': 10.74.0 + '@sentry/opentelemetry': 10.74.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) import-in-the-middle: 3.3.3 optionalDependencies: '@opentelemetry/api': 1.9.1 @@ -8065,72 +8163,67 @@ snapshots: '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.1)(supports-color@7.2.0) '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) - '@sentry/node@10.70.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(supports-color@7.2.0)': + '@sentry/node@10.74.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(supports-color@7.2.0)': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/instrumentation': 0.220.0(@opentelemetry/api@1.9.1)(supports-color@7.2.0) '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) '@sentry/conventions': 0.16.0 - '@sentry/core': 10.70.0 - '@sentry/node-core': 10.70.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1)(supports-color@7.2.0))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) - '@sentry/opentelemetry': 10.70.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) - '@sentry/server-utils': 10.70.0(supports-color@7.2.0) + '@sentry/core': 10.74.0 + '@sentry/node-core': 10.74.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.220.0(@opentelemetry/api@1.9.1)(supports-color@7.2.0))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) + '@sentry/opentelemetry': 10.74.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1)) + '@sentry/server-utils': 10.74.0 import-in-the-middle: 3.3.3 transitivePeerDependencies: - '@opentelemetry/core' - '@opentelemetry/exporter-trace-otlp-http' - supports-color - '@sentry/opentelemetry@10.70.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))': + '@sentry/opentelemetry@10.74.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.10.0(@opentelemetry/api@1.9.1))': dependencies: '@opentelemetry/api': 1.9.1 '@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1) '@opentelemetry/sdk-trace-base': 2.10.0(@opentelemetry/api@1.9.1) '@sentry/conventions': 0.16.0 - '@sentry/core': 10.70.0 + '@sentry/core': 10.74.0 - '@sentry/replay-canvas@10.70.0': + '@sentry/replay-canvas@10.74.0': dependencies: - '@sentry/core': 10.70.0 - '@sentry/replay': 10.70.0 + '@sentry/core': 10.74.0 + '@sentry/replay': 10.74.0 - '@sentry/replay@10.70.0': + '@sentry/replay@10.74.0': dependencies: - '@sentry/browser-utils': 10.70.0 - '@sentry/core': 10.70.0 + '@sentry/browser-utils': 10.74.0 + '@sentry/core': 10.74.0 - '@sentry/server-utils@10.70.0(supports-color@7.2.0)': + '@sentry/server-utils@10.74.0': dependencies: - '@apm-js-collab/code-transformer-bundler-plugins': 0.7.4 - '@apm-js-collab/tracing-hooks': 0.13.0(supports-color@7.2.0) '@sentry/conventions': 0.16.0 - '@sentry/core': 10.70.0 - meriyah: 6.1.4 - transitivePeerDependencies: - - supports-color + '@sentry/core': 10.74.0 - '@sentry/svelte@10.70.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))': + '@sentry/svelte@10.74.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))': dependencies: - '@sentry/browser': 10.70.0 - '@sentry/core': 10.70.0 + '@sentry/browser': 10.74.0 + '@sentry/core': 10.74.0 magic-string: 0.30.21 - svelte: 5.56.10(@typescript-eslint/types@8.67.0) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) - '@sentry/sveltekit@10.70.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(rollup@4.62.4)(supports-color@7.2.0)(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0))': + '@sentry/sveltekit@10.74.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(rollup@4.63.1)(supports-color@7.2.0)(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0))': dependencies: - '@sentry/cloudflare': 10.70.0(supports-color@7.2.0) + '@sentry/cloudflare': 10.74.0 '@sentry/conventions': 0.16.0 - '@sentry/core': 10.70.0 - '@sentry/node': 10.70.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(supports-color@7.2.0) - '@sentry/svelte': 10.70.0(svelte@5.56.10(@typescript-eslint/types@8.67.0)) - '@sentry/vite-plugin': 5.4.0(rollup@4.62.4)(supports-color@7.2.0) + '@sentry/core': 10.74.0 + '@sentry/node': 10.74.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(supports-color@7.2.0) + '@sentry/svelte': 10.74.0(svelte@5.57.0(@typescript-eslint/types@8.70.0)) + '@sentry/vite-plugin': 5.4.0(rollup@4.63.1)(supports-color@7.2.0) '@sveltejs/acorn-typescript': 1.0.13(acorn@8.18.0) - '@sveltejs/kit': 2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) + '@sveltejs/kit': 2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) acorn: 8.18.0 magic-string: 0.30.21 sorcery: 1.0.0 optionalDependencies: - vite: 8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0) + vite: 8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0) transitivePeerDependencies: - '@cloudflare/workers-types' - '@opentelemetry/core' @@ -8142,9 +8235,9 @@ snapshots: - webpack - wrangler - '@sentry/vite-plugin@5.4.0(rollup@4.62.4)(supports-color@7.2.0)': + '@sentry/vite-plugin@5.4.0(rollup@4.63.1)(supports-color@7.2.0)': dependencies: - '@sentry/bundler-plugins': 10.70.0(rollup@4.62.4)(supports-color@7.2.0) + '@sentry/bundler-plugins': 10.72.0(rollup@4.63.1)(supports-color@7.2.0) transitivePeerDependencies: - encoding - rollup @@ -8196,46 +8289,46 @@ snapshots: dependencies: acorn: 8.18.0 - '@sveltejs/adapter-node@5.5.7(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))': + '@sveltejs/adapter-node@5.5.7(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))': dependencies: - '@rollup/plugin-commonjs': 29.0.3(rollup@4.62.4) - '@rollup/plugin-json': 6.1.0(rollup@4.62.4) - '@rollup/plugin-node-resolve': 16.0.3(rollup@4.62.4) - '@rollup/plugin-replace': 6.0.3(rollup@4.62.4) - '@sveltejs/kit': 2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) - rollup: 4.62.4 + '@rollup/plugin-commonjs': 29.0.3(rollup@4.63.1) + '@rollup/plugin-json': 6.1.0(rollup@4.63.1) + '@rollup/plugin-node-resolve': 16.0.3(rollup@4.63.1) + '@rollup/plugin-replace': 6.0.3(rollup@4.63.1) + '@sveltejs/kit': 2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) + rollup: 4.63.1 - '@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0))': + '@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0))': dependencies: '@standard-schema/spec': 1.1.0 '@sveltejs/acorn-typescript': 1.0.13(acorn@8.18.0) - '@sveltejs/vite-plugin-svelte': 7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) + '@sveltejs/vite-plugin-svelte': 7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) '@types/cookie': 0.6.0 acorn: 8.18.0 cookie: 0.6.0 - devalue: 5.9.1 + devalue: 5.9.2 esm-env: 1.2.2 kleur: 4.1.5 magic-string: 0.30.21 mrmime: 2.0.1 set-cookie-parser: 3.1.2 sirv: 3.0.2 - svelte: 5.56.10(@typescript-eslint/types@8.67.0) - vite: 8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) + vite: 8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0) optionalDependencies: '@opentelemetry/api': 1.9.1 typescript: 6.0.3 '@sveltejs/load-config@0.2.3': {} - '@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0))': + '@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0))': dependencies: deepmerge: 4.3.1 - magic-string: 1.2.0 + magic-string: 1.2.3 obug: 2.1.4 - svelte: 5.56.10(@typescript-eslint/types@8.67.0) - vite: 8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0) - vitefu: 1.1.3(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) + vite: 8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0) + vitefu: 1.1.3(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) '@swc/helpers@0.5.15': dependencies: @@ -8306,69 +8399,69 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 - '@tailwindcss/vite@4.3.3(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0))': + '@tailwindcss/vite@4.3.3(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.3.3 '@tailwindcss/oxide': 4.3.3 tailwindcss: 4.3.3 - vite: 8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0) + vite: 8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0) - '@takumi-rs/core-darwin-arm64@2.10.0': + '@takumi-rs/core-darwin-arm64@2.13.7': optional: true - '@takumi-rs/core-darwin-x64@2.10.0': + '@takumi-rs/core-darwin-x64@2.13.7': optional: true - '@takumi-rs/core-linux-arm64-gnu@2.10.0': + '@takumi-rs/core-linux-arm64-gnu@2.13.7': optional: true - '@takumi-rs/core-linux-arm64-musl@2.10.0': + '@takumi-rs/core-linux-arm64-musl@2.13.7': optional: true - '@takumi-rs/core-linux-x64-gnu@2.10.0': {} + '@takumi-rs/core-linux-x64-gnu@2.13.7': {} - '@takumi-rs/core-linux-x64-musl@2.10.0': + '@takumi-rs/core-linux-x64-musl@2.13.7': optional: true - '@takumi-rs/core-win32-arm64-msvc@2.10.0': + '@takumi-rs/core-win32-arm64-msvc@2.13.7': optional: true - '@takumi-rs/core-win32-x64-msvc@2.10.0': + '@takumi-rs/core-win32-x64-msvc@2.13.7': optional: true - '@takumi-rs/core@2.10.0(csstype@3.2.3)(react@19.2.7)': + '@takumi-rs/core@2.13.7(csstype@3.2.3)(react@19.2.7)': dependencies: - '@takumi-rs/helpers': 2.10.0(react@19.2.7) + '@takumi-rs/helpers': 2.13.7(react@19.2.7) optionalDependencies: - '@takumi-rs/core-darwin-arm64': 2.10.0 - '@takumi-rs/core-darwin-x64': 2.10.0 - '@takumi-rs/core-linux-arm64-gnu': 2.10.0 - '@takumi-rs/core-linux-arm64-musl': 2.10.0 - '@takumi-rs/core-linux-x64-gnu': 2.10.0 - '@takumi-rs/core-linux-x64-musl': 2.10.0 - '@takumi-rs/core-win32-arm64-msvc': 2.10.0 - '@takumi-rs/core-win32-x64-msvc': 2.10.0 + '@takumi-rs/core-darwin-arm64': 2.13.7 + '@takumi-rs/core-darwin-x64': 2.13.7 + '@takumi-rs/core-linux-arm64-gnu': 2.13.7 + '@takumi-rs/core-linux-arm64-musl': 2.13.7 + '@takumi-rs/core-linux-x64-gnu': 2.13.7 + '@takumi-rs/core-linux-x64-musl': 2.13.7 + '@takumi-rs/core-win32-arm64-msvc': 2.13.7 + '@takumi-rs/core-win32-x64-msvc': 2.13.7 csstype: 3.2.3 transitivePeerDependencies: - preact - react - '@takumi-rs/helpers@2.10.0(react@19.2.7)': + '@takumi-rs/helpers@2.13.7(react@19.2.7)': optionalDependencies: react: 19.2.7 - '@takumi-rs/wasm@2.10.0(csstype@3.2.3)(react@19.2.7)': + '@takumi-rs/wasm@2.13.7(csstype@3.2.3)(react@19.2.7)': dependencies: - '@takumi-rs/helpers': 2.10.0(react@19.2.7) + '@takumi-rs/helpers': 2.13.7(react@19.2.7) optionalDependencies: csstype: 3.2.3 transitivePeerDependencies: - preact - react - '@testing-library/svelte-core@1.1.3(svelte@5.56.10(@typescript-eslint/types@8.67.0))': + '@testing-library/svelte-core@1.1.3(svelte@5.57.0(@typescript-eslint/types@8.70.0))': dependencies: - svelte: 5.56.10(@typescript-eslint/types@8.67.0) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) '@tokenizer/inflate@0.4.1(supports-color@7.2.0)': dependencies: @@ -8385,7 +8478,7 @@ snapshots: '@types/busboy@1.5.4': dependencies: - '@types/node': 26.2.0 + '@types/node': 26.5.1 '@types/chai@5.2.3': dependencies: @@ -8429,15 +8522,15 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@26.2.0': + '@types/node@26.5.1': dependencies: - undici-types: 8.3.0 + undici-types: 8.9.0 '@types/parse-json@4.0.2': {} '@types/pg@8.15.6': dependencies: - '@types/node': 26.2.0 + '@types/node': 26.5.1 pg-protocol: 1.14.0 pg-types: 2.2.0 optional: true @@ -8463,7 +8556,8 @@ snapshots: fflate: 0.6.11 meshoptimizer: 0.18.1 - '@types/trusted-types@2.0.7': {} + '@types/trusted-types@2.0.7': + optional: true '@types/umami-browser@2.18.1': {} @@ -8482,7 +8576,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 26.2.0 + '@types/node': 26.5.1 '@typeschema/class-validator@0.3.0(@types/json-schema@7.0.15)(class-validator@0.14.4)': dependencies: @@ -8498,15 +8592,15 @@ snapshots: '@types/json-schema': 7.0.15 optional: true - '@typescript-eslint/eslint-plugin@8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.67.0 - '@typescript-eslint/type-utils': 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.67.0 - eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + '@typescript-eslint/parser': 8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.70.0 + '@typescript-eslint/type-utils': 8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.70.0 + eslint: 10.10.0(jiti@2.7.0)(supports-color@7.2.0) ignore: 7.0.6 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -8514,56 +8608,56 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/parser@8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: - '@typescript-eslint/scope-manager': 8.67.0 - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/typescript-estree': 8.67.0(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.67.0 + '@typescript-eslint/scope-manager': 8.70.0 + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/typescript-estree': 8.70.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.70.0 debug: 4.4.3(supports-color@7.2.0) - eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + eslint: 10.10.0(jiti@2.7.0)(supports-color@7.2.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.67.0(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/project-service@8.70.0(supports-color@7.2.0)(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.67.0(typescript@6.0.3) - '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/tsconfig-utils': 8.70.0(typescript@6.0.3) + '@typescript-eslint/types': 8.70.0 debug: 4.4.3(supports-color@7.2.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.67.0': + '@typescript-eslint/scope-manager@8.70.0': dependencies: - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/visitor-keys': 8.67.0 + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/visitor-keys': 8.70.0 - '@typescript-eslint/tsconfig-utils@8.67.0(typescript@6.0.3)': + '@typescript-eslint/tsconfig-utils@8.70.0(typescript@6.0.3)': dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/typescript-estree': 8.67.0(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/typescript-estree': 8.70.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) debug: 4.4.3(supports-color@7.2.0) - eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + eslint: 10.10.0(jiti@2.7.0)(supports-color@7.2.0) ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.67.0': {} + '@typescript-eslint/types@8.70.0': {} - '@typescript-eslint/typescript-estree@8.67.0(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.70.0(supports-color@7.2.0)(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.67.0(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.67.0(typescript@6.0.3) - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/visitor-keys': 8.67.0 + '@typescript-eslint/project-service': 8.70.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.70.0(typescript@6.0.3) + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/visitor-keys': 8.70.0 debug: 4.4.3(supports-color@7.2.0) minimatch: 10.2.6 semver: 7.8.5 @@ -8573,20 +8667,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': + '@typescript-eslint/utils@8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0)) - '@typescript-eslint/scope-manager': 8.67.0 - '@typescript-eslint/types': 8.67.0 - '@typescript-eslint/typescript-estree': 8.67.0(supports-color@7.2.0)(typescript@6.0.3) - eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0)) + '@typescript-eslint/scope-manager': 8.70.0 + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/typescript-estree': 8.70.0(supports-color@7.2.0)(typescript@6.0.3) + eslint: 10.10.0(jiti@2.7.0)(supports-color@7.2.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.67.0': + '@typescript-eslint/visitor-keys@8.70.0': dependencies: - '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/types': 8.70.0 eslint-visitor-keys: 5.0.1 '@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@6.0.3))': @@ -8603,35 +8697,35 @@ snapshots: '@types/validator': 13.15.10 '@vinejs/compiler': 3.0.0 camelcase: 8.0.0 - dayjs: 1.11.21 + dayjs: 1.11.23 dlv: 1.1.3 normalize-url: 8.1.1 validator: 13.15.35 optional: true - '@vitest/browser-playwright@4.1.11(playwright@1.62.1)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.11)': + '@vitest/browser-playwright@4.1.11(playwright@1.63.0)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.11)': dependencies: - '@vitest/browser': 4.1.11(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.11) - '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) - playwright: 1.62.1 + '@vitest/browser': 4.1.11(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.11) + '@vitest/mocker': 4.1.11(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) + playwright: 1.63.0 tinyrainbow: 3.1.1 - vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.2.0)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.11.2)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) + vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.5.1)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.12.0)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser@4.1.11(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.11)': + '@vitest/browser@4.1.11(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.11)': dependencies: '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) + '@vitest/mocker': 4.1.11(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) '@vitest/utils': 4.1.11 magic-string: 0.30.21 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.1 - vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.2.0)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.11.2)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) + vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.5.1)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.12.0)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) ws: 8.21.0 transitivePeerDependencies: - bufferutil @@ -8651,9 +8745,9 @@ snapshots: obug: 2.1.4 std-env: 4.2.0 tinyrainbow: 3.1.1 - vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.2.0)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.11.2)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) + vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.5.1)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.12.0)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) optionalDependencies: - '@vitest/browser': 4.1.11(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.11) + '@vitest/browser': 4.1.11(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.11) '@vitest/expect@4.1.11': dependencies: @@ -8664,13 +8758,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.1 - '@vitest/mocker@4.1.11(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0))': + '@vitest/mocker@4.1.11(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.11 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0) + vite: 8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0) '@vitest/pretty-format@4.1.11': dependencies: @@ -8699,7 +8793,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.17 tinyrainbow: 3.1.1 - vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.2.0)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.11.2)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) + vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.5.1)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.12.0)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) '@vitest/utils@4.1.11': dependencies: @@ -8725,8 +8819,8 @@ snapshots: optionalDependencies: ajv: 8.20.0 - ajv-formats@3.0.1(ajv@8.20.0): - optionalDependencies: + ajv-formats@3.0.1: + dependencies: ajv: 8.20.0 ajv@6.15.0: @@ -8739,14 +8833,14 @@ snapshots: ajv@8.18.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.5 + fast-uri: 3.1.6 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.5 + fast-uri: 3.1.6 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -8785,35 +8879,33 @@ snapshots: estree-walker: 3.0.3 js-tokens: 10.0.0 - astring@1.9.0: {} - atomic-sleep@1.0.0: {} - auth@1.6.29(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(better-call@1.4.0(zod@4.4.3))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(kysely@0.29.5)(postgres@3.4.9))(jose@6.2.8)(kysely@0.29.5)(magicast@0.5.4)(nanostores@1.4.2)(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@7.2.0)(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vitest@4.1.11): + auth@1.7.7(3b94b67bc64ad3733f525f5ab8710cfe): dependencies: '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/preset-react': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0))(supports-color@7.2.0) - '@better-auth/core': 1.6.29(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.5)(nanostores@1.4.2) - '@better-auth/telemetry': 1.6.29(@better-auth/core@1.6.29(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.5)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1) + '@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.2))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) + '@better-auth/telemetry': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.2))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2) '@better-auth/utils': 0.4.2 '@clack/prompts': 1.7.0 - '@mrleebo/prisma-ast': 0.13.1 - better-auth: 1.6.29(@opentelemetry/api@1.9.1)(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(kysely@0.29.5)(postgres@3.4.9))(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vitest@4.1.11) - c12: 3.3.4(magicast@0.5.4) + '@mrleebo/prisma-ast': 0.16.0 + better-auth: 1.7.7(@opentelemetry/api@1.9.1)(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(drizzle-kit@0.31.11)(drizzle-orm@0.45.4(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(kysely@0.29.5)(postgres@3.4.9))(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.63.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vitest@4.1.11) + c12: 4.0.0-beta.5(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magicast@0.5.4) chalk: 5.6.2 - commander: 12.1.0 + commander: 15.0.0 dotenv: 17.4.2 - get-tsconfig: 4.14.2 - open: 10.2.0 + get-tsconfig: 4.14.3 + jiti: 2.7.0 + open: 11.0.2 prettier: 3.9.6 prompts: 2.4.2 semver: 7.8.5 - yocto-spinner: 0.2.3 - zod: 4.4.3 + yocto-spinner: 1.2.2 + zod: 4.6.2 transitivePeerDependencies: - '@better-fetch/fetch' - - '@cloudflare/workers-types' - '@lynx-js/react' - '@opentelemetry/api' - '@prisma/client' @@ -8821,9 +8913,10 @@ snapshots: - '@tanstack/react-start' - '@tanstack/solid-start' - better-call - - better-sqlite3 + - chokidar - drizzle-kit - drizzle-orm + - giget - jose - kysely - magicast @@ -8853,58 +8946,57 @@ snapshots: baseline-browser-mapping@2.10.43: {} - better-auth@1.6.29(@opentelemetry/api@1.9.1)(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(drizzle-kit@0.31.10)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(kysely@0.29.5)(postgres@3.4.9))(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vitest@4.1.11): + better-auth@1.7.7(@opentelemetry/api@1.9.1)(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(drizzle-kit@0.31.11)(drizzle-orm@0.45.4(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(kysely@0.29.5)(postgres@3.4.9))(next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.63.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vitest@4.1.11): dependencies: - '@better-auth/core': 1.6.29(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.5)(nanostores@1.4.2) - '@better-auth/drizzle-adapter': 1.6.29(@better-auth/core@1.6.29(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.5)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(kysely@0.29.5)(postgres@3.4.9)) - '@better-auth/kysely-adapter': 1.6.29(@better-auth/core@1.6.29(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.5)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(kysely@0.29.5) - '@better-auth/memory-adapter': 1.6.29(@better-auth/core@1.6.29(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.5)(nanostores@1.4.2))(@better-auth/utils@0.4.2) - '@better-auth/mongo-adapter': 1.6.29(@better-auth/core@1.6.29(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.5)(nanostores@1.4.2))(@better-auth/utils@0.4.2) - '@better-auth/prisma-adapter': 1.6.29(@better-auth/core@1.6.29(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.5)(nanostores@1.4.2))(@better-auth/utils@0.4.2) - '@better-auth/telemetry': 1.6.29(@better-auth/core@1.6.29(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.4.3))(jose@6.2.8)(kysely@0.29.5)(nanostores@1.4.2))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1) + '@better-auth/core': 1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.2))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2) + '@better-auth/drizzle-adapter': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.2))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(drizzle-orm@0.45.4(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(kysely@0.29.5)(postgres@3.4.9)) + '@better-auth/kysely-adapter': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.2))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(kysely@0.29.5) + '@better-auth/memory-adapter': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.2))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2) + '@better-auth/mongo-adapter': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.2))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2) + '@better-auth/prisma-adapter': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.2))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2) + '@better-auth/telemetry': 1.7.7(@better-auth/core@1.7.7(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2)(@opentelemetry/api@1.9.1)(better-call@1.4.0(zod@4.6.2))(jose@6.2.10)(kysely@0.29.5)(nanostores@1.5.2))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.2) '@better-auth/utils': 0.4.2 - '@better-fetch/fetch': 1.3.1 - '@noble/ciphers': 2.3.0 - '@noble/hashes': 2.3.0 - better-call: 1.4.0(zod@4.4.3) + '@better-fetch/fetch': 1.3.2 + '@noble/ciphers': 2.4.0 + '@noble/hashes': 2.4.0 + better-call: 1.4.0(zod@4.6.2) defu: 6.1.7 - jose: 6.2.8 + jose: 6.2.10 kysely: 0.29.5 - nanostores: 1.4.2 - zod: 4.4.3 + nanostores: 1.5.2 + zod: 4.6.2 optionalDependencies: - '@sveltejs/kit': 2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) - drizzle-kit: 0.31.10 - drizzle-orm: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(kysely@0.29.5)(postgres@3.4.9) - next: 16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4) + '@sveltejs/kit': 2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) + drizzle-kit: 0.31.11 + drizzle-orm: 0.45.4(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(kysely@0.29.5)(postgres@3.4.9) + next: 16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.63.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4) react: 19.2.7 react-dom: 19.2.7(react@19.2.7) - svelte: 5.56.10(@typescript-eslint/types@8.67.0) - vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.2.0)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.11.2)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) + vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.5.1)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.12.0)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) transitivePeerDependencies: - - '@cloudflare/workers-types' - '@opentelemetry/api' - better-call@1.4.0(zod@4.4.3): + better-call@1.4.0(zod@4.6.2): dependencies: '@better-auth/utils': 0.5.0 '@better-fetch/fetch': 1.3.1 rou3: 0.9.2 set-cookie-parser: 3.1.2 optionalDependencies: - zod: 4.4.3 + zod: 4.6.2 binary-extensions@2.3.0: {} - bits-ui@2.19.0(@internationalized/date@3.12.3)(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0)): + bits-ui@2.19.2(@internationalized/date@3.12.4)(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0)): dependencies: '@floating-ui/core': 1.8.0 '@floating-ui/dom': 1.8.0 - '@internationalized/date': 3.12.3 + '@internationalized/date': 3.12.4 esm-env: 1.2.2 - runed: 0.35.1(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0)) - svelte: 5.56.10(@typescript-eslint/types@8.67.0) - svelte-toolbelt: 0.10.6(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0)) + runed: 0.35.1(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0)) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) + svelte-toolbelt: 0.10.6(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0)) tabbable: 6.5.0 transitivePeerDependencies: - '@sveltejs/kit' @@ -8933,7 +9025,7 @@ snapshots: buffer-image-size@0.6.4: dependencies: - '@types/node': 26.2.0 + '@types/node': 26.5.1 bundle-name@4.1.0: dependencies: @@ -8943,25 +9035,31 @@ snapshots: dependencies: streamsearch: 1.1.0 - c12@3.3.4(magicast@0.5.4): + c12@4.0.0-beta.5(chokidar@5.0.0)(dotenv@17.4.2)(giget@3.3.1)(jiti@2.7.0)(magicast@0.5.4): dependencies: - chokidar: 5.0.0 confbox: 0.2.4 defu: 6.1.7 - dotenv: 17.4.2 exsolve: 1.1.1 - giget: 3.3.1 - jiti: 2.7.0 - ohash: 2.0.12 pathe: 2.0.3 - perfect-debounce: 2.1.0 pkg-types: 2.3.1 rc9: 3.0.1 optionalDependencies: + chokidar: 5.0.0 + dotenv: 17.4.2 + giget: 3.3.1 + jiti: 2.7.0 magicast: 0.5.4 cac@7.0.0: {} + cacheable@2.5.0: + dependencies: + '@cacheable/memory': 2.2.0 + '@cacheable/utils': 2.5.0 + hookified: 1.15.1 + keyv: 5.6.0 + qified: 0.10.1 + callsites@3.1.0: {} camelcase@8.0.0: @@ -8992,14 +9090,13 @@ snapshots: charenc@0.0.2: {} - chevrotain@10.5.0: + chevrotain@12.0.0: dependencies: - '@chevrotain/cst-dts-gen': 10.5.0 - '@chevrotain/gast': 10.5.0 - '@chevrotain/types': 10.5.0 - '@chevrotain/utils': 10.5.0 - lodash: 4.17.21 - regexp-to-ast: 0.5.0 + '@chevrotain/cst-dts-gen': 12.0.0 + '@chevrotain/gast': 12.0.0 + '@chevrotain/regexp-to-ast': 12.0.0 + '@chevrotain/types': 12.0.0 + '@chevrotain/utils': 12.0.0 chokidar@3.6.0: dependencies: @@ -9028,7 +9125,7 @@ snapshots: class-validator@0.14.4: dependencies: '@types/validator': 13.15.10 - libphonenumber-js: 1.13.11 + libphonenumber-js: 1.13.12 validator: 13.15.35 optional: true @@ -9044,8 +9141,6 @@ snapshots: colorette@2.0.20: {} - commander@12.1.0: {} - commander@14.0.3: {} commander@15.0.0: {} @@ -9062,13 +9157,13 @@ snapshots: dependencies: simple-wcswidth: 1.1.2 - conventional-changelog-angular@9.3.0: + conventional-changelog-angular@9.4.0: dependencies: - '@conventional-changelog/template': 1.3.0 + '@conventional-changelog/template': 1.4.0 - conventional-changelog-conventionalcommits@10.3.0: + conventional-changelog-conventionalcommits@10.4.0: dependencies: - '@conventional-changelog/template': 1.3.0 + '@conventional-changelog/template': 1.4.0 conventional-commits-parser@7.1.2: dependencies: @@ -9081,9 +9176,9 @@ snapshots: cookie@0.6.0: {} - cosmiconfig-typescript-loader@6.3.0(@types/node@26.2.0)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3): + cosmiconfig-typescript-loader@6.3.0(@types/node@26.5.1)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3): dependencies: - '@types/node': 26.2.0 + '@types/node': 26.5.1 cosmiconfig: 9.0.2(typescript@6.0.3) jiti: 2.6.1 typescript: 6.0.3 @@ -9100,7 +9195,7 @@ snapshots: dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 - js-yaml: 4.3.1 + js-yaml: 4.3.2 parse-json: 5.2.0 optionalDependencies: typescript: 6.0.3 @@ -9137,7 +9232,7 @@ snapshots: dateformat@4.6.3: {} - dayjs@1.11.21: + dayjs@1.11.23: optional: true debug@4.4.3(supports-color@7.2.0): @@ -9156,7 +9251,7 @@ snapshots: default-browser-id@5.0.1: {} - default-browser@5.5.0: + default-browser@5.5.1: dependencies: bundle-name: 4.1.0 default-browser-id: 5.0.1 @@ -9171,7 +9266,7 @@ snapshots: detect-libc@2.1.2: {} - devalue@5.9.1: {} + devalue@5.9.2: {} devlop@1.1.0: dependencies: @@ -9206,14 +9301,14 @@ snapshots: dotenv@17.4.2: {} - drizzle-kit@0.31.10: + drizzle-kit@0.31.11: dependencies: '@drizzle-team/brocli': 0.10.2 '@esbuild-kit/esm-loader': 2.6.5 esbuild: 0.25.12 tsx: 4.23.12 - drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(kysely@0.29.5)(postgres@3.4.9): + drizzle-orm@0.45.4(@opentelemetry/api@1.9.1)(@types/pg@8.15.6)(kysely@0.29.5)(postgres@3.4.9): optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/pg': 8.15.6 @@ -9251,11 +9346,9 @@ snapshots: es-errors@1.3.0: {} - es-module-lexer@2.3.1: {} - es-module-lexer@2.3.2: {} - es-toolkit@1.50.0: {} + es-toolkit@1.52.0: {} esbuild@0.18.20: optionalDependencies: @@ -9346,15 +9439,15 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0)): + eslint-config-prettier@10.1.8(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0)): dependencies: - eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + eslint: 10.10.0(jiti@2.7.0)(supports-color@7.2.0) - eslint-plugin-svelte@3.23.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(svelte@5.56.10(@typescript-eslint/types@8.67.0)): + eslint-plugin-svelte@3.23.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(svelte@5.57.0(@typescript-eslint/types@8.70.0)): dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0)) - '@jridgewell/sourcemap-codec': 1.5.5 - eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0)) + '@jridgewell/sourcemap-codec': 1.6.0 + eslint: 10.10.0(jiti@2.7.0)(supports-color@7.2.0) esutils: 2.0.3 globals: 16.5.0 known-css-properties: 0.37.0 @@ -9362,9 +9455,9 @@ snapshots: postcss-load-config: 3.1.4(postcss@8.5.26) postcss-safe-parser: 7.0.1(postcss@8.5.26) semver: 7.8.5 - svelte-eslint-parser: 1.8.0(svelte@5.56.10(@typescript-eslint/types@8.67.0)) + svelte-eslint-parser: 1.8.1(svelte@5.57.0(@typescript-eslint/types@8.70.0)) optionalDependencies: - svelte: 5.56.10(@typescript-eslint/types@8.67.0) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) transitivePeerDependencies: - ts-node @@ -9386,14 +9479,14 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0): + eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0): dependencies: - '@eslint-community/eslint-utils': 4.10.1(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0)) + '@eslint-community/eslint-utils': 4.10.1(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.23.5(supports-color@7.2.0) '@eslint/config-helpers': 0.7.0 '@eslint/core': 1.2.1 - '@eslint/plugin-kit': 0.7.2 + '@eslint/plugin-kit': 0.7.3 '@humanfs/node': 0.16.8 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 @@ -9408,7 +9501,7 @@ snapshots: esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 + file-entry-cache: 11.1.5 find-up: 5.0.0 glob-parent: 6.0.2 ignore: 5.3.2 @@ -9441,11 +9534,11 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@2.3.5(@typescript-eslint/types@8.67.0): + esrap@2.3.6(@typescript-eslint/types@8.70.0): dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 optionalDependencies: - '@typescript-eslint/types': 8.67.0 + '@typescript-eslint/types': 8.70.0 esrecurse@4.3.0: dependencies: @@ -9478,7 +9571,7 @@ snapshots: is-plain-obj: 4.1.0 is-stream: 4.0.1 npm-run-path: 6.0.0 - pretty-ms: 9.3.0 + pretty-ms: 9.3.1 signal-exit: 4.1.0 strip-final-newline: 4.0.0 yoctocolors: 2.2.0 @@ -9508,15 +9601,15 @@ snapshots: dependencies: fast-string-truncated-width: 3.0.3 - fast-uri@3.1.5: {} + fast-uri@3.1.6: {} fast-wrap-ansi@0.2.2: dependencies: fast-string-width: 3.0.2 - fdir@6.5.0(picomatch@4.0.5): + fdir@6.5.0(picomatch@4.0.7): optionalDependencies: - picomatch: 4.0.5 + picomatch: 4.0.7 fflate@0.6.11: {} @@ -9526,9 +9619,9 @@ snapshots: dependencies: is-unicode-supported: 2.1.0 - file-entry-cache@8.0.0: + file-entry-cache@11.1.5: dependencies: - flat-cache: 4.0.1 + flat-cache: 6.1.23 file-type@21.3.4(supports-color@7.2.0): dependencies: @@ -9555,10 +9648,11 @@ snapshots: locate-path: 8.0.0 unicorn-magic: 0.3.0 - flat-cache@4.0.1: + flat-cache@6.1.23: dependencies: + cacheable: 2.5.0 flatted: 3.4.4 - keyv: 4.5.4 + hookified: 1.15.1 flatted@3.4.4: {} @@ -9566,11 +9660,11 @@ snapshots: dependencies: tabbable: 6.5.0 - formsnap@2.0.1(svelte@5.56.10(@typescript-eslint/types@8.67.0))(sveltekit-superforms@2.30.2(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(@types/json-schema@7.0.15)(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)): + formsnap@2.0.1(svelte@5.57.0(@typescript-eslint/types@8.70.0))(sveltekit-superforms@2.30.2(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(@types/json-schema@7.0.15)(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)): dependencies: - svelte: 5.56.10(@typescript-eslint/types@8.67.0) - svelte-toolbelt: 0.5.0(svelte@5.56.10(@typescript-eslint/types@8.67.0)) - sveltekit-superforms: 2.30.2(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(@types/json-schema@7.0.15)(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) + svelte-toolbelt: 0.5.0(svelte@5.57.0(@typescript-eslint/types@8.70.0)) + sveltekit-superforms: 2.30.2(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(@types/json-schema@7.0.15)(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3) fs-extra@11.4.0: dependencies: @@ -9578,9 +9672,6 @@ snapshots: jsonfile: 6.2.1 universalify: 2.0.1 - fsevents@2.3.2: - optional: true - fsevents@2.3.3: optional: true @@ -9597,7 +9688,7 @@ snapshots: '@sec-ant/readable-stream': 0.4.1 is-stream: 4.0.1 - get-tsconfig@4.14.2: + get-tsconfig@4.14.3: dependencies: resolve-pkg-maps: 1.0.0 @@ -9605,7 +9696,8 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - giget@3.3.1: {} + giget@3.3.1: + optional: true glob-parent@5.1.2: dependencies: @@ -9627,7 +9719,7 @@ snapshots: globals@16.5.0: {} - globals@17.11.0: {} + globals@17.12.0: {} globalyzer@0.1.0: {} @@ -9650,9 +9742,9 @@ snapshots: graphql@16.14.1: {} - happy-dom@20.11.2: + happy-dom@20.12.0: dependencies: - '@types/node': 26.2.0 + '@types/node': 26.5.1 '@types/whatwg-mimetype': 3.0.2 '@types/ws': 8.18.1 buffer-image-size: 0.6.4 @@ -9665,6 +9757,10 @@ snapshots: has-flag@4.0.0: {} + hashery@1.5.1: + dependencies: + hookified: 1.15.1 + hasown@2.0.4: dependencies: function-bind: 1.1.2 @@ -9675,6 +9771,10 @@ snapshots: dependencies: react-is: 16.13.1 + hookified@1.15.1: {} + + hookified@2.2.0: {} + html-escaper@2.0.2: {} http-status@2.1.0: {} @@ -9708,7 +9808,7 @@ snapshots: import-in-the-middle@3.3.3: dependencies: cjs-module-lexer: 2.2.1 - es-module-lexer: 2.3.1 + es-module-lexer: 2.3.2 module-details-from-path: 1.0.4 import-meta-resolve@4.2.0: {} @@ -9752,6 +9852,8 @@ snapshots: is-hexadecimal@2.0.1: {} + is-in-ssh@1.0.0: {} + is-inside-container@1.0.0: dependencies: is-docker: 3.0.0 @@ -9801,7 +9903,7 @@ snapshots: jju@1.4.0: {} - joi@17.13.4: + joi@17.13.6: dependencies: '@hapi/hoek': 9.3.0 '@hapi/topo': 5.1.0 @@ -9812,7 +9914,7 @@ snapshots: jose@5.10.0: {} - jose@6.2.8: {} + jose@6.2.10: {} joycon@3.1.1: {} @@ -9820,14 +9922,12 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@4.3.1: + js-yaml@4.3.2: dependencies: argparse: 2.0.1 jsesc@3.1.0: {} - json-buffer@3.0.1: {} - json-parse-even-better-errors@2.3.1: {} json-schema-to-ts@3.1.1: @@ -9842,7 +9942,7 @@ snapshots: '@types/json-schema': 7.0.15 '@types/lodash': 4.17.24 is-glob: 4.0.3 - js-yaml: 4.3.1 + js-yaml: 4.3.2 lodash: 4.18.1 minimist: 1.2.8 prettier: 3.9.6 @@ -9868,9 +9968,9 @@ snapshots: jsox@1.2.121: {} - keyv@4.5.4: + keyv@5.6.0: dependencies: - json-buffer: 3.0.1 + '@keyv/serialize': 1.1.1 kleur@3.0.3: {} @@ -9878,7 +9978,7 @@ snapshots: known-css-properties@0.37.0: {} - ky@2.0.2: {} + ky@2.1.0: {} kysely@0.29.5: {} @@ -9900,7 +10000,7 @@ snapshots: dependencies: isomorphic.js: 0.2.5 - libphonenumber-js@1.13.11: + libphonenumber-js@1.13.12: optional: true lightningcss-android-arm64@1.32.0: @@ -10019,8 +10119,6 @@ snapshots: dependencies: p-locate: 6.0.0 - lodash@4.17.21: {} - lodash@4.18.1: {} longest-streak@3.1.0: {} @@ -10041,11 +10139,11 @@ snapshots: magic-string@0.30.21: dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 - magic-string@1.2.0: + magic-string@1.2.3: dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 magicast@0.5.4: dependencies: @@ -10057,7 +10155,7 @@ snapshots: dependencies: semver: 7.8.5 - markdown-it@14.3.0: + markdown-it@14.3.1: dependencies: argparse: 2.0.1 entities: 4.5.0 @@ -10152,8 +10250,6 @@ snapshots: memoize-weak@1.0.2: {} - meriyah@6.1.4: {} - meshoptimizer@0.18.1: {} micromark-core-commonmark@2.0.3: @@ -10333,11 +10429,11 @@ snapshots: minipass@7.1.3: {} - mode-watcher@1.1.0(svelte@5.56.10(@typescript-eslint/types@8.67.0)): + mode-watcher@1.1.0(svelte@5.57.0(@typescript-eslint/types@8.70.0)): dependencies: - runed: 0.25.0(svelte@5.56.10(@typescript-eslint/types@8.67.0)) - svelte: 5.56.10(@typescript-eslint/types@8.67.0) - svelte-toolbelt: 0.7.1(svelte@5.56.10(@typescript-eslint/types@8.67.0)) + runed: 0.25.0(svelte@5.57.0(@typescript-eslint/types@8.70.0)) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) + svelte-toolbelt: 0.7.1(svelte@5.57.0(@typescript-eslint/types@8.70.0)) module-details-from-path@1.0.4: {} @@ -10354,11 +10450,11 @@ snapshots: nanoid@3.3.18: {} - nanostores@1.4.2: {} + nanostores@1.5.2: {} natural-compare@1.4.0: {} - next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.62.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4): + next@16.2.7(@babel/core@7.29.7(supports-color@7.2.0))(@opentelemetry/api@1.9.1)(@playwright/test@1.63.0)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(sass@1.77.4): dependencies: '@next/env': 16.2.7 '@swc/helpers': 0.5.15 @@ -10378,7 +10474,7 @@ snapshots: '@next/swc-win32-arm64-msvc': 16.2.7 '@next/swc-win32-x64-msvc': 16.2.7 '@opentelemetry/api': 1.9.1 - '@playwright/test': 1.62.1 + '@playwright/test': 1.63.0 sass: 1.77.4 sharp: 0.34.5 transitivePeerDependencies: @@ -10411,20 +10507,20 @@ snapshots: obug@2.1.4: {} - ohash@2.0.12: {} - on-exit-leak-free@2.1.2: {} once@1.4.0: dependencies: wrappy: 1.0.2 - open@10.2.0: + open@11.0.2: dependencies: - default-browser: 5.5.0 + default-browser: 5.5.1 define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 is-inside-container: 1.0.0 - wsl-utils: 0.1.0 + powershell-utils: 0.2.1 + wsl-utils: 1.0.0 optionator@0.9.4: dependencies: @@ -10435,68 +10531,78 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - orval@8.24.0(prettier@3.9.6)(supports-color@7.2.0)(typescript@6.0.3): + orval@8.31.0(effect@3.22.1)(prettier@3.9.6)(supports-color@7.2.0)(typescript@6.0.3)(zod@4.6.2): dependencies: '@commander-js/extra-typings': 15.0.0(commander@15.0.0) - '@orval/angular': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) - '@orval/axios': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) - '@orval/core': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) - '@orval/effect': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) - '@orval/fetch': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) - '@orval/hono': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) - '@orval/mcp': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) - '@orval/mock': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) - '@orval/query': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) - '@orval/solid-start': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) - '@orval/swr': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) - '@orval/zod': 8.24.0(supports-color@7.2.0)(typescript@6.0.3) + '@orval/angular': 8.31.0(supports-color@7.2.0)(typescript@6.0.3) + '@orval/axios': 8.31.0(supports-color@7.2.0)(typescript@6.0.3) + '@orval/core': 8.31.0(supports-color@7.2.0)(typescript@6.0.3) + '@orval/effect': 8.31.0(effect@3.22.1)(supports-color@7.2.0)(typescript@6.0.3) + '@orval/fetch': 8.31.0(supports-color@7.2.0)(typescript@6.0.3) + '@orval/hono': 8.31.0(supports-color@7.2.0)(typescript@6.0.3)(zod@4.6.2) + '@orval/mcp': 8.31.0(supports-color@7.2.0)(typescript@6.0.3)(zod@4.6.2) + '@orval/mock': 8.31.0(supports-color@7.2.0)(typescript@6.0.3) + '@orval/query': 8.31.0(supports-color@7.2.0)(typescript@6.0.3) + '@orval/solid-start': 8.31.0(supports-color@7.2.0)(typescript@6.0.3) + '@orval/swr': 8.31.0(supports-color@7.2.0)(typescript@6.0.3) + '@orval/zod': 8.31.0(supports-color@7.2.0)(typescript@6.0.3)(zod@4.6.2) '@scalar/json-magic': 0.12.20 - '@scalar/openapi-parser': 0.28.14 + '@scalar/openapi-parser': 0.28.16 '@scalar/openapi-types': 0.9.4 chokidar: 5.0.0 commander: 15.0.0 execa: 9.6.1 find-up: 8.0.0 fs-extra: 11.4.0 - get-tsconfig: 4.14.2 + get-tsconfig: 4.14.3 jiti: 2.7.0 - js-yaml: 4.3.1 - remeda: 2.40.0 + js-yaml: 4.3.2 + remeda: 2.45.0 string-argv: 0.3.2 typedoc: 0.28.20(typescript@6.0.3) typedoc-plugin-coverage: 4.0.3(typedoc@0.28.20(typescript@6.0.3)) - typedoc-plugin-markdown: 4.12.0(typedoc@0.28.20(typescript@6.0.3)) + typedoc-plugin-markdown: 4.13.0(typedoc@0.28.20(typescript@6.0.3)) optionalDependencies: prettier: 3.9.6 transitivePeerDependencies: - '@faker-js/faker' + - '@modelcontextprotocol/sdk' + - '@tanstack/angular-query-experimental' + - '@tanstack/react-query' + - '@tanstack/solid-query' + - '@tanstack/svelte-query' + - '@tanstack/vue-query' + - effect + - msw - supports-color + - swr - typescript + - zod - oxfmt@0.64.0(svelte@5.56.10(@typescript-eslint/types@8.67.0)): + oxfmt@0.67.0(svelte@5.57.0(@typescript-eslint/types@8.70.0)): dependencies: - tinypool: 2.1.0 + tinypool: 2.1.2 optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.64.0 - '@oxfmt/binding-android-arm64': 0.64.0 - '@oxfmt/binding-darwin-arm64': 0.64.0 - '@oxfmt/binding-darwin-x64': 0.64.0 - '@oxfmt/binding-freebsd-x64': 0.64.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.64.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.64.0 - '@oxfmt/binding-linux-arm64-gnu': 0.64.0 - '@oxfmt/binding-linux-arm64-musl': 0.64.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.64.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.64.0 - '@oxfmt/binding-linux-riscv64-musl': 0.64.0 - '@oxfmt/binding-linux-s390x-gnu': 0.64.0 - '@oxfmt/binding-linux-x64-gnu': 0.64.0 - '@oxfmt/binding-linux-x64-musl': 0.64.0 - '@oxfmt/binding-openharmony-arm64': 0.64.0 - '@oxfmt/binding-win32-arm64-msvc': 0.64.0 - '@oxfmt/binding-win32-ia32-msvc': 0.64.0 - '@oxfmt/binding-win32-x64-msvc': 0.64.0 - svelte: 5.56.10(@typescript-eslint/types@8.67.0) + '@oxfmt/binding-android-arm-eabi': 0.67.0 + '@oxfmt/binding-android-arm64': 0.67.0 + '@oxfmt/binding-darwin-arm64': 0.67.0 + '@oxfmt/binding-darwin-x64': 0.67.0 + '@oxfmt/binding-freebsd-x64': 0.67.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.67.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.67.0 + '@oxfmt/binding-linux-arm64-gnu': 0.67.0 + '@oxfmt/binding-linux-arm64-musl': 0.67.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.67.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.67.0 + '@oxfmt/binding-linux-riscv64-musl': 0.67.0 + '@oxfmt/binding-linux-s390x-gnu': 0.67.0 + '@oxfmt/binding-linux-x64-gnu': 0.67.0 + '@oxfmt/binding-linux-x64-musl': 0.67.0 + '@oxfmt/binding-openharmony-arm64': 0.67.0 + '@oxfmt/binding-win32-arm64-msvc': 0.67.0 + '@oxfmt/binding-win32-ia32-msvc': 0.67.0 + '@oxfmt/binding-win32-x64-msvc': 0.67.0 + svelte: 5.57.0(@typescript-eslint/types@8.70.0) p-limit@3.1.0: dependencies: @@ -10516,11 +10622,11 @@ snapshots: package-manager-detector@1.8.0: {} - paneforge@1.0.2(svelte@5.56.10(@typescript-eslint/types@8.67.0)): + paneforge@1.0.2(svelte@5.57.0(@typescript-eslint/types@8.70.0)): dependencies: - runed: 0.23.4(svelte@5.56.10(@typescript-eslint/types@8.67.0)) - svelte: 5.56.10(@typescript-eslint/types@8.67.0) - svelte-toolbelt: 0.9.3(svelte@5.56.10(@typescript-eslint/types@8.67.0)) + runed: 0.23.4(svelte@5.57.0(@typescript-eslint/types@8.70.0)) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) + svelte-toolbelt: 0.9.3(svelte@5.57.0(@typescript-eslint/types@8.70.0)) parent-module@1.0.1: dependencies: @@ -10564,10 +10670,10 @@ snapshots: pathe@2.0.3: {} - payload@3.88.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3): + payload@3.89.0(graphql@16.14.1)(supports-color@7.2.0)(typescript@6.0.3): dependencies: '@next/env': 15.5.19 - '@payloadcms/translations': 3.88.0 + '@payloadcms/translations': 3.89.0 '@types/busboy': 1.5.4 ajv: 8.18.0 bson-objectid: 2.0.4 @@ -10604,8 +10710,6 @@ snapshots: - typescript - utf-8-validate - perfect-debounce@2.1.0: {} - pg-int8@1.0.1: optional: true @@ -10625,7 +10729,7 @@ snapshots: picomatch@2.3.2: {} - picomatch@4.0.5: {} + picomatch@4.0.7: {} pino-abstract-transport@2.0.0: dependencies: @@ -10669,13 +10773,11 @@ snapshots: exsolve: 1.1.1 pathe: 2.0.3 - playwright-core@1.62.1: {} + playwright-core@1.63.0: {} - playwright@1.62.1: + playwright@1.63.0: dependencies: - playwright-core: 1.62.1 - optionalDependencies: - fsevents: 2.3.2 + playwright-core: 1.63.0 pluralize@8.0.0: {} @@ -10713,6 +10815,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.28: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postgres-array@2.0.0: optional: true @@ -10729,11 +10837,15 @@ snapshots: postgres@3.4.9: {} + powershell-utils@0.1.0: {} + + powershell-utils@0.2.1: {} + prelude-ls@1.2.1: {} prettier@3.9.6: {} - pretty-ms@9.3.0: + pretty-ms@9.3.1: dependencies: parse-ms: 4.0.0 @@ -10771,6 +10883,10 @@ snapshots: pure-rand@6.1.0: optional: true + qified@0.10.1: + dependencies: + hookified: 2.2.0 + qs-esm@8.0.1: {} quick-format-unescaped@4.0.4: {} @@ -10800,9 +10916,11 @@ snapshots: '@babel/runtime': 7.29.7 react: 19.2.7 - react-error-boundary@6.1.2(react@19.2.7): + react-error-boundary@6.1.3(@types/react@19.2.16)(react@19.2.7): dependencies: react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.16 react-image-crop@10.1.8(react@19.2.7): dependencies: @@ -10848,9 +10966,7 @@ snapshots: real-require@0.2.0: {} - regexp-to-ast@0.5.0: {} - - remeda@2.40.0: {} + remeda@2.45.0: {} require-from-string@2.0.2: {} @@ -10874,101 +10990,101 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - rolldown@1.2.5: + rolldown@1.2.6: dependencies: - '@oxc-project/types': 0.146.0 + '@oxc-project/types': 0.147.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm-eabi': 1.2.5 - '@rolldown/binding-android-arm64': 1.2.5 - '@rolldown/binding-darwin-arm64': 1.2.5 - '@rolldown/binding-darwin-x64': 1.2.5 - '@rolldown/binding-freebsd-x64': 1.2.5 - '@rolldown/binding-linux-arm-gnueabihf': 1.2.5 - '@rolldown/binding-linux-arm64-gnu': 1.2.5 - '@rolldown/binding-linux-arm64-musl': 1.2.5 - '@rolldown/binding-linux-ppc64-gnu': 1.2.5 - '@rolldown/binding-linux-s390x-gnu': 1.2.5 - '@rolldown/binding-linux-x64-gnu': 1.2.5 - '@rolldown/binding-linux-x64-musl': 1.2.5 - '@rolldown/binding-openharmony-arm64': 1.2.5 - '@rolldown/binding-win32-arm64-msvc': 1.2.5 - '@rolldown/binding-win32-x64-msvc': 1.2.5 - - rollup@4.62.4: + '@rolldown/binding-android-arm-eabi': 1.2.6 + '@rolldown/binding-android-arm64': 1.2.6 + '@rolldown/binding-darwin-arm64': 1.2.6 + '@rolldown/binding-darwin-x64': 1.2.6 + '@rolldown/binding-freebsd-x64': 1.2.6 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.6 + '@rolldown/binding-linux-arm64-gnu': 1.2.6 + '@rolldown/binding-linux-arm64-musl': 1.2.6 + '@rolldown/binding-linux-ppc64-gnu': 1.2.6 + '@rolldown/binding-linux-s390x-gnu': 1.2.6 + '@rolldown/binding-linux-x64-gnu': 1.2.6 + '@rolldown/binding-linux-x64-musl': 1.2.6 + '@rolldown/binding-openharmony-arm64': 1.2.6 + '@rolldown/binding-win32-arm64-msvc': 1.2.6 + '@rolldown/binding-win32-x64-msvc': 1.2.6 + + rollup@4.63.1: dependencies: '@types/estree': 1.0.9 optionalDependencies: '@napi-rs/lzma-linux-x64-gnu': 1.5.1 - '@rollup/rollup-android-arm-eabi': 4.62.4 - '@rollup/rollup-android-arm64': 4.62.4 - '@rollup/rollup-darwin-arm64': 4.62.4 - '@rollup/rollup-darwin-x64': 4.62.4 - '@rollup/rollup-freebsd-arm64': 4.62.4 - '@rollup/rollup-freebsd-x64': 4.62.4 - '@rollup/rollup-linux-arm-gnueabihf': 4.62.4 - '@rollup/rollup-linux-arm-musleabihf': 4.62.4 - '@rollup/rollup-linux-arm64-gnu': 4.62.4 - '@rollup/rollup-linux-arm64-musl': 4.62.4 - '@rollup/rollup-linux-loong64-gnu': 4.62.4 - '@rollup/rollup-linux-loong64-musl': 4.62.4 - '@rollup/rollup-linux-ppc64-gnu': 4.62.4 - '@rollup/rollup-linux-ppc64-musl': 4.62.4 - '@rollup/rollup-linux-riscv64-gnu': 4.62.4 - '@rollup/rollup-linux-riscv64-musl': 4.62.4 - '@rollup/rollup-linux-s390x-gnu': 4.62.4 - '@rollup/rollup-linux-x64-gnu': 4.62.4 - '@rollup/rollup-linux-x64-musl': 4.62.4 - '@rollup/rollup-openbsd-x64': 4.62.4 - '@rollup/rollup-openharmony-arm64': 4.62.4 - '@rollup/rollup-win32-arm64-msvc': 4.62.4 - '@rollup/rollup-win32-ia32-msvc': 4.62.4 - '@rollup/rollup-win32-x64-gnu': 4.62.4 - '@rollup/rollup-win32-x64-msvc': 4.62.4 + '@rollup/rollup-android-arm-eabi': 4.63.1 + '@rollup/rollup-android-arm64': 4.63.1 + '@rollup/rollup-darwin-arm64': 4.63.1 + '@rollup/rollup-darwin-x64': 4.63.1 + '@rollup/rollup-freebsd-arm64': 4.63.1 + '@rollup/rollup-freebsd-x64': 4.63.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.63.1 + '@rollup/rollup-linux-arm-musleabihf': 4.63.1 + '@rollup/rollup-linux-arm64-gnu': 4.63.1 + '@rollup/rollup-linux-arm64-musl': 4.63.1 + '@rollup/rollup-linux-loong64-gnu': 4.63.1 + '@rollup/rollup-linux-loong64-musl': 4.63.1 + '@rollup/rollup-linux-ppc64-gnu': 4.63.1 + '@rollup/rollup-linux-ppc64-musl': 4.63.1 + '@rollup/rollup-linux-riscv64-gnu': 4.63.1 + '@rollup/rollup-linux-riscv64-musl': 4.63.1 + '@rollup/rollup-linux-s390x-gnu': 4.63.1 + '@rollup/rollup-linux-x64-gnu': 4.63.1 + '@rollup/rollup-linux-x64-musl': 4.63.1 + '@rollup/rollup-openbsd-x64': 4.63.1 + '@rollup/rollup-openharmony-arm64': 4.63.1 + '@rollup/rollup-win32-arm64-msvc': 4.63.1 + '@rollup/rollup-win32-ia32-msvc': 4.63.1 + '@rollup/rollup-win32-x64-gnu': 4.63.1 + '@rollup/rollup-win32-x64-msvc': 4.63.1 fsevents: 2.3.3 rou3@0.9.2: {} run-applescript@7.1.0: {} - runed@0.23.4(svelte@5.56.10(@typescript-eslint/types@8.67.0)): + runed@0.23.4(svelte@5.57.0(@typescript-eslint/types@8.70.0)): dependencies: esm-env: 1.2.2 - svelte: 5.56.10(@typescript-eslint/types@8.67.0) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) - runed@0.25.0(svelte@5.56.10(@typescript-eslint/types@8.67.0)): + runed@0.25.0(svelte@5.57.0(@typescript-eslint/types@8.70.0)): dependencies: esm-env: 1.2.2 - svelte: 5.56.10(@typescript-eslint/types@8.67.0) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) - runed@0.28.0(svelte@5.56.10(@typescript-eslint/types@8.67.0)): + runed@0.28.0(svelte@5.57.0(@typescript-eslint/types@8.70.0)): dependencies: esm-env: 1.2.2 - svelte: 5.56.10(@typescript-eslint/types@8.67.0) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) - runed@0.29.2(svelte@5.56.10(@typescript-eslint/types@8.67.0)): + runed@0.29.2(svelte@5.57.0(@typescript-eslint/types@8.70.0)): dependencies: esm-env: 1.2.2 - svelte: 5.56.10(@typescript-eslint/types@8.67.0) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) - runed@0.35.1(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0)): + runed@0.35.1(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0)): dependencies: dequal: 2.0.3 esm-env: 1.2.2 lz-string: 1.5.0 - svelte: 5.56.10(@typescript-eslint/types@8.67.0) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) optionalDependencies: - '@sveltejs/kit': 2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) + '@sveltejs/kit': 2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) - runed@0.37.1(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(zod@4.4.3): + runed@0.37.1(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(zod@4.6.2): dependencies: dequal: 2.0.3 esm-env: 1.2.2 lz-string: 1.5.0 - svelte: 5.56.10(@typescript-eslint/types@8.67.0) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) optionalDependencies: - '@sveltejs/kit': 2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) - zod: 4.4.3 + '@sveltejs/kit': 2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) + zod: 4.6.2 sade@1.8.1: dependencies: @@ -10994,20 +11110,18 @@ snapshots: secure-json-parse@4.1.0: {} - semifies@1.0.0: {} - semver@6.3.1: {} semver@7.8.5: {} set-cookie-parser@3.1.2: {} - shadcn-svelte@1.5.0(svelte@5.56.10(@typescript-eslint/types@8.67.0)): + shadcn-svelte@1.6.1(svelte@5.57.0(@typescript-eslint/types@8.70.0)): dependencies: commander: 14.0.3 node-fetch-native: 1.6.7 postcss: 8.5.26 - svelte: 5.56.10(@typescript-eslint/types@8.67.0) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) tailwind-merge: 3.6.0 sharp@0.34.5: @@ -11054,7 +11168,7 @@ snapshots: signal-exit@4.1.0: {} - simple-git-hooks@2.13.1: {} + simple-git-hooks@2.14.0: {} simple-wcswidth@1.1.2: {} @@ -11085,7 +11199,7 @@ snapshots: sorcery@1.0.0: dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 minimist: 1.2.8 tiny-glob: 0.2.9 @@ -11165,20 +11279,20 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-check@4.7.6(picomatch@4.0.5)(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3): + svelte-check@4.7.6(picomatch@4.0.7)(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 '@sveltejs/load-config': 0.2.3 chokidar: 4.0.3 - fdir: 6.5.0(picomatch@4.0.5) + fdir: 6.5.0(picomatch@4.0.7) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.56.10(@typescript-eslint/types@8.67.0) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) typescript: 6.0.3 transitivePeerDependencies: - picomatch - svelte-eslint-parser@1.8.0(svelte@5.56.10(@typescript-eslint/types@8.67.0)): + svelte-eslint-parser@1.8.1(svelte@5.57.0(@typescript-eslint/types@8.70.0)): dependencies: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 @@ -11188,69 +11302,68 @@ snapshots: postcss-selector-parser: 7.1.5 semver: 7.8.5 optionalDependencies: - svelte: 5.56.10(@typescript-eslint/types@8.67.0) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) - svelte-persisted-store@0.12.0(svelte@5.56.10(@typescript-eslint/types@8.67.0)): + svelte-persisted-store@0.12.0(svelte@5.57.0(@typescript-eslint/types@8.70.0)): dependencies: - svelte: 5.56.10(@typescript-eslint/types@8.67.0) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) - svelte-seo@2.0.0(svelte@5.56.10(@typescript-eslint/types@8.67.0)): + svelte-seo@2.0.0(svelte@5.57.0(@typescript-eslint/types@8.70.0)): dependencies: schema-dts: 1.1.5 - svelte: 5.56.10(@typescript-eslint/types@8.67.0) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) - svelte-sonner@1.2.1(svelte@5.56.10(@typescript-eslint/types@8.67.0)): + svelte-sonner@1.2.1(svelte@5.57.0(@typescript-eslint/types@8.70.0)): dependencies: - runed: 0.28.0(svelte@5.56.10(@typescript-eslint/types@8.67.0)) - svelte: 5.56.10(@typescript-eslint/types@8.67.0) + runed: 0.28.0(svelte@5.57.0(@typescript-eslint/types@8.70.0)) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) - svelte-tiny-virtual-list@4.0.0(svelte@5.56.10(@typescript-eslint/types@8.67.0)): + svelte-tiny-virtual-list@4.0.0(svelte@5.57.0(@typescript-eslint/types@8.70.0)): dependencies: - svelte: 5.56.10(@typescript-eslint/types@8.67.0) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) - svelte-toolbelt@0.10.6(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0)): + svelte-toolbelt@0.10.6(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0)): dependencies: clsx: 2.1.1 - runed: 0.35.1(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0)) + runed: 0.35.1(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0)) style-to-object: 1.0.14 - svelte: 5.56.10(@typescript-eslint/types@8.67.0) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) transitivePeerDependencies: - '@sveltejs/kit' - svelte-toolbelt@0.5.0(svelte@5.56.10(@typescript-eslint/types@8.67.0)): + svelte-toolbelt@0.5.0(svelte@5.57.0(@typescript-eslint/types@8.70.0)): dependencies: clsx: 2.1.1 style-to-object: 1.0.14 - svelte: 5.56.10(@typescript-eslint/types@8.67.0) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) - svelte-toolbelt@0.7.1(svelte@5.56.10(@typescript-eslint/types@8.67.0)): + svelte-toolbelt@0.7.1(svelte@5.57.0(@typescript-eslint/types@8.70.0)): dependencies: clsx: 2.1.1 - runed: 0.23.4(svelte@5.56.10(@typescript-eslint/types@8.67.0)) + runed: 0.23.4(svelte@5.57.0(@typescript-eslint/types@8.70.0)) style-to-object: 1.0.14 - svelte: 5.56.10(@typescript-eslint/types@8.67.0) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) - svelte-toolbelt@0.9.3(svelte@5.56.10(@typescript-eslint/types@8.67.0)): + svelte-toolbelt@0.9.3(svelte@5.57.0(@typescript-eslint/types@8.70.0)): dependencies: clsx: 2.1.1 - runed: 0.29.2(svelte@5.56.10(@typescript-eslint/types@8.67.0)) + runed: 0.29.2(svelte@5.57.0(@typescript-eslint/types@8.70.0)) style-to-object: 1.0.14 - svelte: 5.56.10(@typescript-eslint/types@8.67.0) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) - svelte@5.56.10(@typescript-eslint/types@8.67.0): + svelte@5.57.0(@typescript-eslint/types@8.70.0): dependencies: '@jridgewell/remapping': 2.3.5 - '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/sourcemap-codec': 1.6.0 '@sveltejs/acorn-typescript': 1.0.13(acorn@8.18.0) '@types/estree': 1.0.9 - '@types/trusted-types': 2.0.7 acorn: 8.18.0 aria-query: 5.3.1 axobject-query: 4.1.0 clsx: 2.1.1 - devalue: 5.9.1 + devalue: 5.9.2 esm-env: 1.2.2 - esrap: 2.3.5(@typescript-eslint/types@8.67.0) + esrap: 2.3.6(@typescript-eslint/types@8.70.0) is-reference: 3.0.3 locate-character: 3.0.0 magic-string: 0.30.21 @@ -11258,12 +11371,12 @@ snapshots: transitivePeerDependencies: - '@typescript-eslint/types' - sveltekit-superforms@2.30.2(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(@types/json-schema@7.0.15)(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3): + sveltekit-superforms@2.30.2(@sveltejs/kit@2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(@types/json-schema@7.0.15)(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3): dependencies: - '@sveltejs/kit': 2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.56.10(@typescript-eslint/types@8.67.0))(typescript@6.0.3)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) - devalue: 5.9.1 + '@sveltejs/kit': 2.70.3(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)))(svelte@5.57.0(@typescript-eslint/types@8.70.0))(typescript@6.0.3)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) + devalue: 5.9.2 memoize-weak: 1.0.2 - svelte: 5.56.10(@typescript-eslint/types@8.67.0) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) ts-deepmerge: 8.0.0 optionalDependencies: '@exodus/schemasafe': 1.3.0 @@ -11274,14 +11387,14 @@ snapshots: arktype: 2.2.3 class-validator: 0.14.4 effect: 3.22.1 - joi: 17.13.4 + joi: 17.13.6 json-schema-to-ts: 3.1.1 superstruct: 2.0.2 - typebox: 1.3.14 + typebox: 1.3.22 valibot: 1.4.2(typescript@6.0.3) yup: 1.7.1 - zod: 4.4.3 - zod-v3-to-json-schema: 4.0.0(zod@4.4.3) + zod: 4.6.2 + zod-v3-to-json-schema: 4.0.0(zod@4.6.2) transitivePeerDependencies: - '@types/json-schema' - typescript @@ -11301,11 +11414,11 @@ snapshots: tailwindcss@4.3.3: {} - takumi-js@2.10.0(csstype@3.2.3)(react@19.2.7): + takumi-js@2.13.7(csstype@3.2.3)(react@19.2.7): dependencies: - '@takumi-rs/core': 2.10.0(csstype@3.2.3)(react@19.2.7) - '@takumi-rs/helpers': 2.10.0(react@19.2.7) - '@takumi-rs/wasm': 2.10.0(csstype@3.2.3)(react@19.2.7) + '@takumi-rs/core': 2.13.7(csstype@3.2.3)(react@19.2.7) + '@takumi-rs/helpers': 2.13.7(react@19.2.7) + '@takumi-rs/wasm': 2.13.7(csstype@3.2.3)(react@19.2.7) transitivePeerDependencies: - csstype - preact @@ -11333,10 +11446,10 @@ snapshots: tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.5) - picomatch: 4.0.5 + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 - tinypool@2.1.0: {} + tinypool@2.1.2: {} tinyrainbow@3.1.1: {} @@ -11397,14 +11510,14 @@ snapshots: type-fest@2.19.0: optional: true - typebox@1.3.14: + typebox@1.3.22: optional: true typedoc-plugin-coverage@4.0.3(typedoc@0.28.20(typescript@6.0.3)): dependencies: typedoc: 0.28.20(typescript@6.0.3) - typedoc-plugin-markdown@4.12.0(typedoc@0.28.20(typescript@6.0.3)): + typedoc-plugin-markdown@4.13.0(typedoc@0.28.20(typescript@6.0.3)): dependencies: typedoc: 0.28.20(typescript@6.0.3) @@ -11412,18 +11525,18 @@ snapshots: dependencies: '@gerrit0/mini-shiki': 3.23.0 lunr: 2.3.9 - markdown-it: 14.3.0 + markdown-it: 14.3.1 minimatch: 10.2.6 typescript: 6.0.3 yaml: 2.9.0 - typescript-eslint@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3): + typescript-eslint@8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.67.0(@typescript-eslint/parser@8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/parser': 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.67.0(supports-color@7.2.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.67.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) - eslint: 10.8.1(jiti@2.7.0)(supports-color@7.2.0) + '@typescript-eslint/eslint-plugin': 8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.70.0(supports-color@7.2.0)(typescript@6.0.3) + '@typescript-eslint/utils': 8.70.0(eslint@10.10.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) + eslint: 10.10.0(jiti@2.7.0)(supports-color@7.2.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -11434,7 +11547,7 @@ snapshots: uint8array-extras@1.5.0: {} - undici-types@8.3.0: {} + undici-types@8.9.0: {} undici@7.29.0: {} @@ -11500,26 +11613,26 @@ snapshots: validator@13.15.35: optional: true - vaul-svelte@1.0.0-next.7(svelte@5.56.10(@typescript-eslint/types@8.67.0)): + vaul-svelte@1.0.0-next.7(svelte@5.57.0(@typescript-eslint/types@8.70.0)): dependencies: - runed: 0.23.4(svelte@5.56.10(@typescript-eslint/types@8.67.0)) - svelte: 5.56.10(@typescript-eslint/types@8.67.0) - svelte-toolbelt: 0.7.1(svelte@5.56.10(@typescript-eslint/types@8.67.0)) + runed: 0.23.4(svelte@5.57.0(@typescript-eslint/types@8.70.0)) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) + svelte-toolbelt: 0.7.1(svelte@5.57.0(@typescript-eslint/types@8.70.0)) vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 unist-util-stringify-position: 4.0.0 - vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0): + vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 - picomatch: 4.0.5 - postcss: 8.5.26 - rolldown: 1.2.5 + picomatch: 4.0.7 + postcss: 8.5.28 + rolldown: 1.2.6 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 26.2.0 + '@types/node': 26.5.1 esbuild: 0.28.2 fsevents: 2.3.3 jiti: 2.7.0 @@ -11527,20 +11640,20 @@ snapshots: tsx: 4.23.12 yaml: 2.9.0 - vitefu@1.1.3(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)): + vitefu@1.1.3(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)): optionalDependencies: - vite: 8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0) + vite: 8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0) - vitest-browser-svelte@3.0.0(svelte@5.56.10(@typescript-eslint/types@8.67.0))(vitest@4.1.11): + vitest-browser-svelte@3.1.0(svelte@5.57.0(@typescript-eslint/types@8.70.0))(vitest@4.1.11): dependencies: - '@testing-library/svelte-core': 1.1.3(svelte@5.56.10(@typescript-eslint/types@8.67.0)) - svelte: 5.56.10(@typescript-eslint/types@8.67.0) - vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.2.0)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.11.2)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) + '@testing-library/svelte-core': 1.1.3(svelte@5.57.0(@typescript-eslint/types@8.70.0)) + svelte: 5.57.0(@typescript-eslint/types@8.70.0) + vitest: 4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.5.1)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.12.0)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) - vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.2.0)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.11.2)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)): + vitest@4.1.11(@opentelemetry/api@1.9.1)(@types/node@26.5.1)(@vitest/browser-playwright@4.1.11)(@vitest/coverage-v8@4.1.11)(@vitest/ui@4.1.11)(happy-dom@20.12.0)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) + '@vitest/mocker': 4.1.11(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.11 '@vitest/runner': 4.1.11 '@vitest/snapshot': 4.1.11 @@ -11551,21 +11664,21 @@ snapshots: magic-string: 0.30.21 obug: 2.1.4 pathe: 2.0.3 - picomatch: 4.0.5 + picomatch: 4.0.7 std-env: 4.2.0 tinybench: 2.9.0 tinyexec: 1.3.0 tinyglobby: 0.2.17 tinyrainbow: 3.1.1 - vite: 8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0) + vite: 8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.1 - '@types/node': 26.2.0 - '@vitest/browser-playwright': 4.1.11(playwright@1.62.1)(vite@8.2.2(@types/node@26.2.0)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.11) + '@types/node': 26.5.1 + '@vitest/browser-playwright': 4.1.11(playwright@1.63.0)(vite@8.3.0(@types/node@26.5.1)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.77.4)(tsx@4.23.12)(yaml@2.9.0))(vitest@4.1.11) '@vitest/coverage-v8': 4.1.11(@vitest/browser@4.1.11)(vitest@4.1.11) '@vitest/ui': 4.1.11(vitest@4.1.11) - happy-dom: 20.11.2 + happy-dom: 20.12.0 transitivePeerDependencies: - msw @@ -11599,9 +11712,10 @@ snapshots: ws@8.21.0: {} - wsl-utils@0.1.0: + wsl-utils@1.0.0: dependencies: is-wsl: 3.1.1 + powershell-utils: 0.1.0 xss@1.0.15: dependencies: @@ -11638,7 +11752,7 @@ snapshots: yocto-queue@1.2.2: {} - yocto-spinner@0.2.3: + yocto-spinner@1.2.2: dependencies: yoctocolors: 2.2.0 @@ -11654,11 +11768,11 @@ snapshots: zimmerframe@1.1.4: {} - zod-v3-to-json-schema@4.0.0(zod@4.4.3): + zod-v3-to-json-schema@4.0.0(zod@4.6.2): dependencies: - zod: 4.4.3 + zod: 4.6.2 optional: true - zod@4.4.3: {} + zod@4.6.2: {} zwitch@2.0.4: {} diff --git a/src/app.d.ts b/src/app.d.ts index f9b2ae91b..343545bbc 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,4 +1,8 @@ -import type { Session, User } from "better-auth/minimal"; +import type { auth } from "$lib/server/auth"; +import type { DiscordProfileAssets } from "$lib/server/discord-profile"; +import type { McIdLink, McIdProfile } from "$lib/server/minecraft-accounts"; + +type AuthSession = typeof auth.$Infer.Session; // See https://kit.svelte.dev/docs/types#app // for information about these interfaces @@ -16,8 +20,11 @@ declare global { } interface Locals { - user?: User; - session?: Session; + pendingDiscordProfile?: DiscordProfileAssets; + pendingMcIdProfile?: McIdProfile; + pendingMcIdLink?: McIdLink; + user?: AuthSession["user"]; + session?: AuthSession["session"]; } } } diff --git a/src/lib/components/dashboard/EmojiEditor.svelte b/src/lib/components/dashboard/EmojiEditor.svelte new file mode 100644 index 000000000..c43ae7501 --- /dev/null +++ b/src/lib/components/dashboard/EmojiEditor.svelte @@ -0,0 +1,96 @@ + + +
    + Stats for + + + + {account.username} + {#if account.entitlementLevel > 0} + + {valid ? parsed.data : (account.emoji ?? "")} + + {/if} + + + + selected, + (v) => { + popoverOpen = false; + selected = v; + tick().then(() => save()); + } + } + class="contents"> + +
    + {#if account.entitlementLevel >= 2} + + + {/if} +
    + + = 2 ? undefined : coloredHearts} /> + {#if account.entitlementLevel >= 2} + + {#snippet children({ active })} +
    + {active?.emoji} + + {active?.data.name} + +
    + {/snippet} +
    + {/if} +
    +
    +
    +
    +
    diff --git a/src/lib/components/dashboard/EmojiEditor.svelte.spec.ts b/src/lib/components/dashboard/EmojiEditor.svelte.spec.ts new file mode 100644 index 000000000..b2abf9611 --- /dev/null +++ b/src/lib/components/dashboard/EmojiEditor.svelte.spec.ts @@ -0,0 +1,96 @@ +import { beforeEach, expect, it, vi } from "vitest"; +import { render } from "vitest-browser-svelte"; +import { page, userEvent } from "vitest/browser"; +import EmojiEditor from "./EmojiEditor.svelte"; +import "../../../routes/layout.css"; + +const mocks = vi.hoisted(() => ({ updateEmoji: vi.fn() })); +vi.mock("$routes/(protected)/dashboard/donator-perks/perks.remote", () => ({ updateEmoji: mocks.updateEmoji })); +vi.mock("svelte-sonner", () => ({ toast: { success: vi.fn() } })); + +const account = { uuid: "4855c53ee4fb4100997600a92fc50984", username: "EmojiTest", emoji: "❤️", entitlementLevel: 1 }; +beforeEach(() => vi.resetAllMocks()); + +it("lets heart supporters preview and save a different heart", async () => { + await page.viewport(320, 844); + const view = await render(EmojiEditor, { account }); + await expect.element(page.getByRole("button", { name: "Save emoji" })).toBeDisabled(); + await expect.element(page.getByRole("combobox", { name: "Search emojis" })).toBeVisible(); + expect(page.getByRole("option").all().length).toBe(12); + const options = document.querySelectorAll('[role="option"]'); + expect(options[0].getBoundingClientRect().right).toBeLessThanOrEqual(options[1].getBoundingClientRect().left); + await expect.element(page.getByRole("button", { name: "Choose any emoji", exact: true })).not.toBeInTheDocument(); + await page.getByRole("option", { name: "Purple Heart", exact: true }).click(); + await expect.element(page.getByText("Save to apply your preview")).not.toBeInTheDocument(); + await expect.element(page.getByLabelText("Selected emoji", { exact: true })).toHaveTextContent("💜"); + await page.getByRole("button", { name: "Save emoji" }).click(); + expect(mocks.updateEmoji).toHaveBeenCalledWith({ uuid: account.uuid, emoji: "💜" }); + expect(document.documentElement.scrollWidth).toBeLessThanOrEqual(320); + await view.unmount(); +}); + +it("searches the picker, previews an emoji, and retains it after a save failure", async () => { + const view = await render(EmojiEditor, { account: { ...account, entitlementLevel: 2 } }); + const search = page.getByRole("combobox", { name: "Search emojis" }); + await search.fill("duck"); + await page.getByRole("option", { name: "Duck", exact: true }).click(); + await expect.element(page.getByLabelText("Selected emoji", { exact: true })).toHaveTextContent("🦆"); + await expect.element(search).toBeVisible(); + mocks.updateEmoji.mockRejectedValueOnce(new Error("Please try again.")); + await page.getByRole("button", { name: "Save emoji" }).click(); + await expect.element(page.getByRole("alert")).toHaveTextContent("Please try again."); + await expect.element(page.getByLabelText("Selected emoji", { exact: true })).toHaveTextContent("🦆"); + await page.getByRole("button", { name: "Save emoji" }).click(); + expect(mocks.updateEmoji).toHaveBeenLastCalledWith({ uuid: account.uuid, emoji: "🦆" }); + await view.unmount(); +}); + +it("shows the locked state without edit controls", async () => { + const view = await render(EmojiEditor, { account: { ...account, emoji: null, entitlementLevel: 0 } }); + await expect.element(page.getByRole("button", { name: "Save emoji" })).not.toBeInTheDocument(); + await expect.element(page.getByText("Unlock a colored heart at $5 total support")).toBeVisible(); + await view.unmount(); +}); + +it("applies skin tones and supports keyboard selection in the picker", async () => { + const view = await render(EmojiEditor, { account: { ...account, entitlementLevel: 2 } }); + await page.getByRole("button", { name: "Change skin tone" }).click(); + const search = page.getByRole("combobox", { name: "Search emojis" }); + await search.fill("wave"); + await search.click(); + await page.getByRole("option", { name: "Waving Hand", exact: true }).click(); + await expect.element(page.getByLabelText("Selected emoji", { exact: true })).toHaveTextContent("👋🏻"); + await page.getByRole("combobox", { name: "Search emojis" }).fill("duck"); + await userEvent.keyboard("{ArrowDown}{Enter}"); + await expect.element(page.getByLabelText("Selected emoji", { exact: true })).toHaveTextContent("🦆"); + await view.unmount(); +}); + +it("only offers colored hearts to heart supporters, including when searching", async () => { + const view = await render(EmojiEditor, { account }); + const search = page.getByRole("combobox", { name: "Search emojis" }); + await search.fill("duck"); + await expect.element(page.getByText("No results.")).toBeVisible(); + await expect.element(page.getByRole("option", { name: "Duck", exact: true })).not.toBeInTheDocument(); + await search.fill("blue"); + await page.getByRole("option", { name: "Blue Heart", exact: true }).click(); + await expect.element(page.getByLabelText("Selected emoji", { exact: true })).toHaveTextContent("💙"); + await view.unmount(); +}); + +it("preselects an emoji for the footer when opening and reopening the popover", async () => { + await render(EmojiEditor, { account: { ...account, entitlementLevel: 2 } }); + const trigger = page.getByRole("button", { name: "Edit profile emoji" }); + await trigger.click(); + await expect.element(page.getByText("Grinning Face", { exact: true })).toBeVisible(); + const firstOption = document.querySelector('[role="option"][data-selected]'); + expect(firstOption?.getAttribute("aria-label")).toBe("Grinning Face"); + expect(mocks.updateEmoji).not.toHaveBeenCalled(); + await page.getByRole("combobox", { name: "Search emojis" }).fill("duck"); + await expect.element(page.getByText("Duck", { exact: true })).toBeVisible(); + await userEvent.keyboard("{Escape}"); + await expect.element(page.getByRole("combobox", { name: "Search emojis" })).not.toBeInTheDocument(); + await trigger.click(); + await expect.element(page.getByText("Grinning Face", { exact: true })).toBeVisible(); + expect(mocks.updateEmoji).not.toHaveBeenCalled(); +}); diff --git a/src/lib/hooks/use-frecency.svelte.ts b/src/lib/hooks/use-frecency.svelte.ts new file mode 100644 index 000000000..5f78f3de5 --- /dev/null +++ b/src/lib/hooks/use-frecency.svelte.ts @@ -0,0 +1,55 @@ +import { PersistedState } from "runed"; + +type FrecencyItem = { + uses: number; + lastUsage: number; +}; + +type PersistedStateOptions = ConstructorParameters>[2]; + +type UseFrecencyOptions = PersistedStateOptions & { + maxItems?: number; +}; + +type FrecencyMap = Record; + +export class UseFrecency { + #items: PersistedState; + + constructor( + key: string, + initialValue: FrecencyMap = {}, + readonly opts: UseFrecencyOptions = {} + ) { + this.#items = new PersistedState(key, initialValue, this.opts); + + this.use = this.use.bind(this); + } + + use(key: string) { + const item = this.#items.current[key]; + + this.#items.current[key] = { + uses: 1 + (item?.uses ?? 0), + lastUsage: Date.now() + }; + } + + get items() { + return Array.from(Object.entries(this.#items.current)) + .filter(([_a, a]) => a !== undefined) + .sort(([_a, a], [_b, b]) => { + if (a!.uses > b!.uses) return -1; + + if (b!.uses > a!.uses) return 1; + + return a!.lastUsage - b!.lastUsage; + }) + .slice(0, this.opts.maxItems) + .map(([key]) => key); + } + + clear() { + this.#items.current = {}; + } +} diff --git a/src/lib/layouts/stats/PlayerProfile.svelte b/src/lib/layouts/stats/PlayerProfile.svelte index c5f15089d..8e526a7e6 100644 --- a/src/lib/layouts/stats/PlayerProfile.svelte +++ b/src/lib/layouts/stats/PlayerProfile.svelte @@ -2,12 +2,14 @@ import { resolve } from "$app/paths"; import { getFavorites, getHoverContext, getProfileContext } from "$ctx"; import { APINotice } from "$lib/components/notices"; + import { getProfileEmoji } from "$lib/shared/profile-emoji.remote"; import { cn } from "$lib/shared/utils"; import CardBuilder from "$src/lib/components/stats/CardBuilder.svelte"; import * as Avatar from "$ui/avatar"; import { Button } from "$ui/button"; import * as Item from "$ui/item"; import * as Popover from "$ui/popover"; + import { Spinner } from "$ui/spinner"; import Ban from "@lucide/svelte/icons/ban"; import ChevronRight from "@lucide/svelte/icons/chevron-right"; import ExternalLink from "@lucide/svelte/icons/external-link"; @@ -80,7 +82,20 @@ {/if} - {profile?.displayName} + + {profile?.username} + {#if profile?.uuid} + + {const emoji = await getProfileEmoji(profile.uuid)} + {#if emoji} + {emoji} + {/if} + {#snippet pending()} + + {/snippet} + + {/if} + ({ db: undefined as unknown })); +vi.mock("$lib/server/db", () => ({ + get db() { + return database.db; + } +})); +import { deleteAccountData, exportAccountData } from "./account-data"; + +describe.skipIf(!process.env.DATABASE_URL)("Account data portability and deletion", () => { + const namespace = `privacy_test_${randomUUID().replaceAll("-", "")}`; + let admin: ReturnType; + let client: ReturnType; + let db: ReturnType>; + const owner = randomUUID(); + const stranger = randomUUID(); + const accountId = randomUUID(); + const uuid = randomUUID().replaceAll("-", ""); + beforeAll(async () => { + admin = postgres(process.env.DATABASE_URL!, { onnotice: () => {} }); + await admin`CREATE SCHEMA ${admin(namespace)}`; + client = postgres(process.env.DATABASE_URL!, { connection: { search_path: namespace }, onnotice: () => {} }); + db = drizzle(client, { schema }); + database.db = db; + for (const file of readdirSync("drizzle") + .filter((file) => file.endsWith(".sql")) + .sort()) { + const sql = readFileSync(`drizzle/${file}`, "utf8").replaceAll('"public".', `"${namespace}".`); + for (const statement of sql.split("--> statement-breakpoint")) await client.unsafe(statement); + } + await db.insert(schema.user).values([ + { id: owner, name: "Owner", email: `${owner}@example.invalid` }, + { id: stranger, name: "Other user", email: `${stranger}@example.invalid` } + ]); + await db.insert(schema.account).values({ + id: accountId, + userId: owner, + accountId: "provider-id", + providerId: "mc-id", + accessToken: "oauth-secret", + refreshToken: "refresh-secret", + idToken: "id-secret", + password: "password-secret" + }); + await db.insert(schema.minecraftAccount).values({ accountId, uuid, username: "LinkedPlayer", primary: true }); + await db.insert(schema.profileEmoji).values({ uuid, emoji: "💜" }); + await db + .insert(schema.discordProfile) + .values({ userId: owner, discordUserId: "discord-id", username: "LinkedPlayer" }); + await db.insert(schema.patreonMembership).values({ + id: randomUUID(), + userId: owner, + patreonUserId: "patreon-id", + memberId: "member-id", + campaignId: "campaign-id" + }); + for (const userId of [owner, stranger]) { + await db.insert(schema.session).values({ + id: randomUUID(), + userId, + token: `session-secret-${userId}`, + expiresAt: new Date(Date.now() + 86400000) + }); + await db.insert(schema.apikey).values({ + id: randomUUID(), + referenceId: userId, + key: `key-secret-${userId}`, + createdAt: new Date(), + updatedAt: new Date() + }); + } + }); + afterAll(async () => { + if (client) await client.end(); + if (admin) { + await admin`DROP SCHEMA IF EXISTS ${admin(namespace)} CASCADE`; + await admin.end(); + } + }); + it("exports personal records without secrets or other users' data, then removes linked data atomically", async () => { + const exported = await exportAccountData(owner); + expect(exported.profile?.id).toBe(owner); + expect(exported.accounts).toHaveLength(1); + expect(exported.minecraftAccounts[0].uuid).toBe(uuid); + expect(exported.profileEmojis[0].emoji).toBe("💜"); + expect(exported.discordProfile?.discordUserId).toBe("discord-id"); + expect(exported.patreonMembership?.memberId).toBe("member-id"); + expect(exported.sessions).toHaveLength(1); + expect(exported.apiKeys).toHaveLength(1); + expect(JSON.stringify(exported)).not.toMatch(/secret|password|accessToken|refreshToken|idToken/); + expect(JSON.stringify(exported)).not.toContain(stranger); + await client.unsafe( + `CREATE FUNCTION reject_privacy_deletion() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'Deletion rejected'; END; $$` + ); + await client.unsafe( + `CREATE TRIGGER reject_privacy_deletion BEFORE DELETE ON "user" FOR EACH ROW EXECUTE FUNCTION reject_privacy_deletion()` + ); + await expect(deleteAccountData(owner)).rejects.toMatchObject({ cause: { message: "Deletion rejected" } }); + expect(await db.select().from(schema.profileEmoji)).toHaveLength(1); + expect(await db.select().from(schema.apikey).where(eq(schema.apikey.referenceId, owner))).toHaveLength(1); + await client.unsafe(`DROP TRIGGER reject_privacy_deletion ON "user"`); + await deleteAccountData(owner); + for (const table of [ + schema.user, + schema.account, + schema.minecraftAccount, + schema.profileEmoji, + schema.discordProfile, + schema.patreonMembership, + schema.session, + schema.apikey + ]) { + const remaining = await db.select().from(table); + expect(JSON.stringify(remaining)).not.toContain(owner); + if (![schema.user, schema.session, schema.apikey].includes(table as typeof schema.user)) + expect(remaining).toHaveLength(0); + } + expect(await db.select().from(schema.user).where(eq(schema.user.id, stranger))).toHaveLength(1); + expect(await db.select().from(schema.session).where(eq(schema.session.userId, stranger))).toHaveLength(1); + expect(await db.select().from(schema.apikey).where(eq(schema.apikey.referenceId, stranger))).toHaveLength(1); + }); +}); diff --git a/src/lib/server/account-data.ts b/src/lib/server/account-data.ts new file mode 100644 index 000000000..ba1ee2c45 --- /dev/null +++ b/src/lib/server/account-data.ts @@ -0,0 +1,116 @@ +import { db } from "$lib/server/db"; +import { + account, + apikey, + discordProfile, + minecraftAccount, + patreonMembership, + profileEmoji, + session, + user +} from "$lib/server/db/schema"; +import { eq, inArray } from "drizzle-orm"; + +export async function exportAccountData(userId: string) { + return db.transaction( + async (tx) => { + const [profile] = await tx.select().from(user).where(eq(user.id, userId)); + const accounts = await tx + .select({ + id: account.id, + accountId: account.accountId, + providerId: account.providerId, + scope: account.scope, + createdAt: account.createdAt, + updatedAt: account.updatedAt + }) + .from(account) + .where(eq(account.userId, userId)); + const minecraftAccounts = await tx + .select({ + accountId: minecraftAccount.accountId, + uuid: minecraftAccount.uuid, + username: minecraftAccount.username, + primary: minecraftAccount.primary, + syncedAt: minecraftAccount.syncedAt + }) + .from(minecraftAccount) + .innerJoin(account, eq(minecraftAccount.accountId, account.id)) + .where(eq(account.userId, userId)); + const uuids = minecraftAccounts.map(({ uuid }) => uuid); + const [discord, membership, sessions, apiKeys, emojis] = await Promise.all([ + tx.select().from(discordProfile).where(eq(discordProfile.userId, userId)), + tx.select().from(patreonMembership).where(eq(patreonMembership.userId, userId)), + tx + .select({ + id: session.id, + userAgent: session.userAgent, + ipAddress: session.ipAddress, + createdAt: session.createdAt, + updatedAt: session.updatedAt, + expiresAt: session.expiresAt, + impersonatedBy: session.impersonatedBy + }) + .from(session) + .where(eq(session.userId, userId)), + tx + .select({ + id: apikey.id, + name: apikey.name, + prefix: apikey.prefix, + enabled: apikey.enabled, + createdAt: apikey.createdAt, + updatedAt: apikey.updatedAt, + expiresAt: apikey.expiresAt, + permissions: apikey.permissions, + metadata: apikey.metadata, + requestCount: apikey.requestCount, + lastRequest: apikey.lastRequest, + remaining: apikey.remaining, + rateLimitEnabled: apikey.rateLimitEnabled, + rateLimitTimeWindow: apikey.rateLimitTimeWindow, + rateLimitMax: apikey.rateLimitMax, + refillInterval: apikey.refillInterval, + refillAmount: apikey.refillAmount, + lastRefillAt: apikey.lastRefillAt + }) + .from(apikey) + .where(eq(apikey.referenceId, userId)), + uuids.length ? tx.select().from(profileEmoji).where(inArray(profileEmoji.uuid, uuids)) : Promise.resolve([]) + ]); + return { + formatVersion: 1, + exportedAt: new Date(), + profile, + accounts, + minecraftAccounts, + discordProfile: discord[0] ?? null, + patreonMembership: membership[0] ?? null, + sessions, + apiKeys, + profileEmojis: emojis + }; + }, + { isolationLevel: "repeatable read", accessMode: "read only" } + ); +} + +export async function deleteAccountData(userId: string) { + await db.transaction(async (tx) => { + const linked = await tx + .select({ uuid: minecraftAccount.uuid }) + .from(minecraftAccount) + .innerJoin(account, eq(minecraftAccount.accountId, account.id)) + .where(eq(account.userId, userId)); + if (linked.length) + await tx.delete(profileEmoji).where( + inArray( + profileEmoji.uuid, + linked.map(({ uuid }) => uuid) + ) + ); + await tx.delete(apikey).where(eq(apikey.referenceId, userId)); + // Foreign keys remove sessions, provider accounts, Minecraft links, Discord data and membership together. + await tx.delete(user).where(eq(user.id, userId)); + }); +} diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index 80d3c17b4..d6a1a7e0b 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -3,11 +3,21 @@ import { getRequestEvent } from "$app/server"; import { env as envPrivate } from "$env/dynamic/private"; import { env as envPublic } from "$env/dynamic/public"; import { db } from "$lib/server/db"; +import { getDiscordProfile, parseDiscordProfile, syncDiscordProfile } from "$lib/server/discord-profile"; +import { + fetchMcIdProfile, + getStoredMinecraftAccounts, + parseMcIdProfile, + storeMinecraftAccounts, + type McIdLink +} from "$lib/server/minecraft-accounts"; import { apiKey } from "@better-auth/api-key"; -import type { BetterAuthPlugin } from "better-auth"; +import type { BetterAuthOptions, BetterAuthPlugin } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { createAuthMiddleware } from "better-auth/api"; import { betterAuth } from "better-auth/minimal"; -import { admin, genericOAuth, patreon } from "better-auth/plugins"; +import { admin, customSession, genericOAuth, patreon } from "better-auth/plugins"; +import { captcha } from "better-auth/plugins"; import { sveltekitCookies } from "better-auth/svelte-kit"; const { @@ -20,7 +30,8 @@ const { MC_ID_CLIENT_SECRET, PATREON_CLIENT_ID, PATREON_CLIENT_SECRET, - DISCORD_WHITELIST + DISCORD_WHITELIST, + TURNSTILE_SECRET_KEY } = envPrivate; const { PUBLIC_BASE_URL } = envPublic; @@ -63,12 +74,32 @@ const openAPIPlugin: BetterAuthPlugin | null = dev })() : null; -export const auth = betterAuth({ +// Account hooks run after Better Auth has validated ownership and persisted the link. +async function queueMinecraftSync(link: McIdLink) { + if (link.providerId !== "mc-id") return; + const { locals } = getRequestEvent(); + if (locals.pendingMcIdProfile?.sub === link.accountId) { + locals.pendingMcIdLink = { + id: link.id, + accountId: link.accountId, + userId: link.userId, + providerId: link.providerId + }; + } +} + +const options = { appName: "SkyCrypt", baseURL, secret: getBetterAuthSecret(), database: drizzleAdapter(db, { provider: "pg" }), emailAndPassword: { enabled: false }, + account: { + accountLinking: { + trustedProviders: ["patreon", "mc-id"], + allowDifferentEmails: true + } + }, socialProviders: { discord: { clientId: DISCORD_CLIENT_ID, @@ -78,13 +109,50 @@ export const auth = betterAuth({ scope: ["identify"], enabled: true, prompt: "consent", - mapProfileToUser: (profile) => ({ - email: profile.id + "@discord.placeholder.local" - }) + overrideUserInfoOnSignIn: true, + mapProfileToUser: (profile) => { + const assets = parseDiscordProfile(profile); + if (assets) getRequestEvent().locals.pendingDiscordProfile = assets; + return { + email: profile.id + "@discord.placeholder.invalid" + }; + } } }, + hooks: { + after: createAuthMiddleware(async (ctx) => { + if (!ctx.path.startsWith("/callback/")) return; + const { locals } = getRequestEvent(); + const mcProfile = locals.pendingMcIdProfile; + const mcLink = locals.pendingMcIdLink; + delete locals.pendingMcIdProfile; + delete locals.pendingMcIdLink; + if (mcProfile && mcLink) { + try { + await storeMinecraftAccounts(mcLink, mcProfile); + } catch (error) { + console.error("Unable to sync linked Minecraft accounts", error); + } + } + if (!ctx.context.newSession) return; + const assets = locals.pendingDiscordProfile; + delete locals.pendingDiscordProfile; + if (!assets) return; + + try { + await syncDiscordProfile(ctx.context.newSession.user.id, assets); + } catch (error) { + // Cosmetic data must not turn a successful sign-in into an error. + console.error("Unable to sync Discord profile assets", error); + } + }) + }, // Better Auth lifecycle database hooks databaseHooks: { + account: { + create: { after: queueMinecraftSync }, + update: { after: queueMinecraftSync } + }, user: { create: { before: async (user) => { @@ -124,7 +192,18 @@ export const auth = betterAuth({ }, plugins: [ apiKey({ - defaultPrefix: "sky_" + defaultPrefix: "skycrypt_", + enableMetadata: true, + requireName: true, + rateLimit: { + enabled: true, + maxRequests: 300, + timeWindow: 1000 * 60 * 5 // 5 minutes + }, + startingCharactersConfig: { + charactersLength: 15, + shouldStore: true + } }), admin(), genericOAuth({ @@ -133,12 +212,25 @@ export const auth = betterAuth({ providerId: "mc-id", clientId: MC_ID_CLIENT_ID, clientSecret: MC_ID_CLIENT_SECRET, + tokenEndpointAuth: { method: "client_secret_basic" }, discoveryUrl: "https://mc-id.com/.well-known/openid-configuration", - scopes: ["profile", "connections"], + scopes: ["openid", "profile", "connections", "offline_access"], + // Accounts live in userinfo; an ID token alone may omit them. + getUserInfo: async ({ accessToken }) => { + if (!accessToken) return null; + const profile = await fetchMcIdProfile(accessToken); + return profile + ? { ...profile, name: profile.name ?? "", emailVerified: profile.email_verified ?? false } + : null; + }, + mapProfileToUser: (profile) => { + const minecraft = parseMcIdProfile(profile); + if (minecraft) getRequestEvent().locals.pendingMcIdProfile = minecraft; + return {}; + }, pkce: true, disableSignUp: true, - disableImplicitSignUp: true, - requireIssuerValidation: true + disableImplicitSignUp: true }, patreon({ clientId: PATREON_CLIENT_ID, @@ -146,11 +238,33 @@ export const auth = betterAuth({ pkce: true, disableSignUp: true, disableImplicitSignUp: true, - scopes: ["identity.memberships"] + scopes: ["identity", "identity[email]"] }) ] }), - ...(openAPIPlugin ? [openAPIPlugin] : []), // Conditionally include the openAPI plugin only in development + captcha({ + provider: "cloudflare-turnstile", + secretKey: TURNSTILE_SECRET_KEY + // endpoints: ["/sign-in/social", "/api-key/create"] + }), + ...(openAPIPlugin ? [openAPIPlugin] : []) // Conditionally include the openAPI plugin only in development + ] +} satisfies BetterAuthOptions; + +export const auth = betterAuth({ + ...options, + plugins: [ + ...(options.plugins ?? []), + customSession(async ({ user, session }) => { + const [discordProfile, minecraftAccounts] = await Promise.all([ + getDiscordProfile(user.id), + getStoredMinecraftAccounts(user.id) + ]); + return { + user: { ...user, discordProfile, minecraftAccounts }, + session + }; + }, options), sveltekitCookies(getRequestEvent) // make sure this is the last plugin in the array ] }); diff --git a/src/lib/server/db/schema/auth.schema.ts b/src/lib/server/db/schema/auth.schema.ts index 223648617..4dbaaf75e 100644 --- a/src/lib/server/db/schema/auth.schema.ts +++ b/src/lib/server/db/schema/auth.schema.ts @@ -1,5 +1,5 @@ import { relations } from "drizzle-orm"; -import { boolean, index, integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; +import { pgTable, text, timestamp, boolean, integer, index } from "drizzle-orm/pg-core"; export const user = pgTable("user", { id: text("id").primaryKey(), @@ -91,10 +91,10 @@ export const apikey = pgTable( refillInterval: integer("refill_interval"), refillAmount: integer("refill_amount"), lastRefillAt: timestamp("last_refill_at"), - enabled: boolean("enabled").default(true), + enabled: boolean("enabled").default(false), rateLimitEnabled: boolean("rate_limit_enabled").default(true), - rateLimitTimeWindow: integer("rate_limit_time_window").default(86400000), - rateLimitMax: integer("rate_limit_max").default(10), + rateLimitTimeWindow: integer("rate_limit_time_window").default(300000), + rateLimitMax: integer("rate_limit_max").default(300), requestCount: integer("request_count").default(0), remaining: integer("remaining"), lastRequest: timestamp("last_request"), diff --git a/src/lib/server/db/schema/discord.schema.ts b/src/lib/server/db/schema/discord.schema.ts new file mode 100644 index 000000000..73db8fa20 --- /dev/null +++ b/src/lib/server/db/schema/discord.schema.ts @@ -0,0 +1,20 @@ +import { jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core"; +import { user } from "./auth.schema"; + +export const discordProfile = pgTable("discord_profile", { + userId: text("user_id") + .primaryKey() + .references(() => user.id, { onDelete: "cascade" }), + discordUserId: text("discord_user_id").notNull().unique(), + username: text("username"), + primaryGuild: jsonb("primary_guild").$type<{ + identity_guild_id: string | null; + identity_enabled: boolean | null; + tag: string | null; + badge: string | null; + }>(), + avatarHash: text("avatar_hash"), + bannerHash: text("banner_hash"), + avatarDecorationHash: text("avatar_decoration_hash"), + syncedAt: timestamp("synced_at").defaultNow().notNull() +}); diff --git a/src/lib/server/db/schema/index.ts b/src/lib/server/db/schema/index.ts index 5d13be3df..32fbbac14 100644 --- a/src/lib/server/db/schema/index.ts +++ b/src/lib/server/db/schema/index.ts @@ -1 +1,5 @@ export * from "./auth.schema"; +export * from "./discord.schema"; +export * from "./patreon.schema"; +export * from "./minecraft.schema"; +export * from "./profile-emoji.schema"; diff --git a/src/lib/server/db/schema/minecraft.schema.ts b/src/lib/server/db/schema/minecraft.schema.ts new file mode 100644 index 000000000..355d606c0 --- /dev/null +++ b/src/lib/server/db/schema/minecraft.schema.ts @@ -0,0 +1,16 @@ +import { boolean, pgTable, primaryKey, text, timestamp } from "drizzle-orm/pg-core"; +import { account } from "./auth.schema"; + +export const minecraftAccount = pgTable( + "minecraft_account", + { + accountId: text("account_id") + .notNull() + .references(() => account.id, { onDelete: "cascade" }), + uuid: text("uuid").notNull(), + username: text("username").notNull(), + primary: boolean("primary").notNull(), + syncedAt: timestamp("synced_at").defaultNow().notNull() + }, + (table) => [primaryKey({ columns: [table.accountId, table.uuid] })] +); diff --git a/src/lib/server/db/schema/patreon.schema.ts b/src/lib/server/db/schema/patreon.schema.ts new file mode 100644 index 000000000..163f7d840 --- /dev/null +++ b/src/lib/server/db/schema/patreon.schema.ts @@ -0,0 +1,38 @@ +import { index, integer, pgTable, text, timestamp, uniqueIndex } from "drizzle-orm/pg-core"; +import { user } from "./auth.schema"; + +export const patreonMembership = pgTable( + "patreon_membership", + { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + patreonUserId: text("patreon_user_id").notNull(), + memberId: text("member_id").notNull(), + campaignId: text("campaign_id").notNull(), + lifetimeSupportCents: integer("lifetime_support_cents").default(0).notNull(), + currentlyEntitledAmountCents: integer("currently_entitled_amount_cents").default(0).notNull(), + patronStatus: text("patron_status"), + entitlementLevel: integer("entitlement_level").default(0).notNull(), + verifiedAt: timestamp("verified_at").defaultNow().notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull() + }, + (table) => [ + uniqueIndex("patreon_membership_userId_uidx").on(table.userId), + uniqueIndex("patreon_membership_memberId_uidx").on(table.memberId), + index("patreon_membership_patreonUserId_idx").on(table.patreonUserId) + ] +); + +export const patreonCreatorToken = pgTable("patreon_creator_token", { + campaignId: text("campaign_id").primaryKey(), + accessToken: text("access_token").notNull(), + refreshToken: text("refresh_token").notNull(), + expiresAt: timestamp("expires_at"), + updatedAt: timestamp("updated_at").defaultNow().notNull() +}); diff --git a/src/lib/server/db/schema/profile-emoji.schema.ts b/src/lib/server/db/schema/profile-emoji.schema.ts new file mode 100644 index 000000000..e4c18937f --- /dev/null +++ b/src/lib/server/db/schema/profile-emoji.schema.ts @@ -0,0 +1,14 @@ +import { sql } from "drizzle-orm"; +import { check, integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; + +// UUID grants also cover donations outside Patreon and manually granted perks. +export const profileEmoji = pgTable( + "profile_emoji", + { + uuid: text("uuid").primaryKey(), + emoji: text("emoji").notNull(), + entitlementLevel: integer("entitlement_level").default(1).notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull() + }, + (table) => [check("profile_emoji_entitlement_check", sql`${table.entitlementLevel} between 1 and 2`)] +); diff --git a/src/lib/server/discord-profile.spec.ts b/src/lib/server/discord-profile.spec.ts new file mode 100644 index 000000000..1e2a47cb4 --- /dev/null +++ b/src/lib/server/discord-profile.spec.ts @@ -0,0 +1,195 @@ +import { discordProfile } from "$lib/server/db/schema"; +import { PgDialect } from "drizzle-orm/pg-core"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { getDiscordProfile, parseDiscordProfile, syncDiscordProfile } from "./discord-profile"; + +const database = vi.hoisted(() => ({ + insert: vi.fn(), + values: vi.fn(), + onConflictDoUpdate: vi.fn(), + select: vi.fn(), + from: vi.fn(), + where: vi.fn(), + limit: vi.fn() +})); + +vi.mock("$lib/server/db", () => ({ db: database })); + +beforeEach(() => { + vi.resetAllMocks(); + database.insert.mockReturnValue(database); + database.values.mockReturnValue(database); + database.onConflictDoUpdate.mockResolvedValue(undefined); + database.select.mockReturnValue(database); + database.from.mockReturnValue(database); + database.where.mockReturnValue(database); + database.limit.mockResolvedValue([]); +}); + +describe("parseDiscordProfile", () => { + it("reads modern Discord assets, preferring decoration data over the legacy field", () => { + expect( + parseDiscordProfile({ + id: "123456789012345678", + avatar: "a_avatar123", + banner: "banner456", + avatar_decoration: "legacy", + avatar_decoration_data: { asset: "a_decoration789", sku_id: "987" }, + username: "skycrypt" + }) + ).toEqual({ + discordUserId: "123456789012345678", + username: "skycrypt", + primaryGuild: null, + avatarHash: "a_avatar123", + bannerHash: "banner456", + avatarDecorationHash: "a_decoration789" + }); + }); + + it("preserves explicit removal even when the legacy decoration field is present", () => { + expect( + parseDiscordProfile({ + id: "123", + avatar: null, + banner: null, + avatar_decoration: "stale", + avatar_decoration_data: null + }) + ).toEqual({ + discordUserId: "123", + username: null, + primaryGuild: null, + avatarHash: null, + bannerHash: null, + avatarDecorationHash: null + }); + }); + + it("falls back to the legacy decoration only when modern data is absent", () => { + expect(parseDiscordProfile({ id: "123", avatar_decoration: "legacy" })).toEqual({ + discordUserId: "123", + username: null, + primaryGuild: null, + avatarHash: null, + bannerHash: null, + avatarDecorationHash: "legacy" + }); + }); + + it("accepts accounts without custom assets", () => { + expect(parseDiscordProfile({ id: "123" })).toEqual({ + discordUserId: "123", + username: null, + primaryGuild: null, + avatarHash: null, + bannerHash: null, + avatarDecorationHash: null + }); + }); + + it("stores username and the complete primary guild without mixing in clan data", () => { + const primaryGuild = { + identity_guild_id: "332309672486895637", + identity_enabled: true, + tag: "APPL", + badge: "fb7ebd9358a8c1f1e8d23547333a86f0" + }; + expect( + parseDiscordProfile({ + id: "389759544776982528", + username: "tim_cook", + primary_guild: primaryGuild, + clan: { tag: "OTHER" } + }) + ).toMatchObject({ username: "tim_cook", primaryGuild }); + }); + + it("preserves disabled guild identity and nullable guild details", () => { + expect( + parseDiscordProfile({ + id: "123", + primary_guild: { + identity_guild_id: null, + identity_enabled: false, + tag: null, + badge: null + } + })?.primaryGuild + ).toEqual({ + identity_guild_id: null, + identity_enabled: false, + tag: null, + badge: null + }); + }); + + it("clears a removed primary guild on the next sync and updates the username", async () => { + const profile = parseDiscordProfile({ id: "123", username: "new_name", primary_guild: null }); + expect(profile?.primaryGuild).toBeNull(); + await syncDiscordProfile("authenticated-user", profile!); + expect(database.onConflictDoUpdate).toHaveBeenCalledWith({ + target: discordProfile.userId, + set: expect.objectContaining({ username: "new_name", primaryGuild: null }) + }); + }); + + it.each([ + null, + {}, + { id: 123 }, + { id: "not-a-discord-id" }, + { id: "123", avatar: "../image" }, + { id: "123", banner: "https://example.com/image" }, + { id: "123", avatar_decoration_data: { asset: "" } }, + { id: "123", avatar_decoration_data: {} }, + { id: "123", username: 123 }, + { id: "123", primary_guild: { identity_enabled: "false" } }, + { id: "123", primary_guild: { identity_guild_id: 123 } }, + { id: "123", primary_guild: { badge: "../image" } } + ])("rejects malformed provider data: %j", (profile) => { + expect(parseDiscordProfile(profile)).toBeNull(); + }); +}); + +describe("Discord profile persistence", () => { + it("upserts by the authenticated user and clears assets removed on Discord", async () => { + const assets = { + discordUserId: "123", + username: null, + primaryGuild: null, + avatarHash: null, + bannerHash: null, + avatarDecorationHash: null + }; + + await syncDiscordProfile("authenticated-user", assets); + + expect(database.insert).toHaveBeenCalledWith(discordProfile); + expect(database.values).toHaveBeenCalledWith({ + userId: "authenticated-user", + ...assets, + syncedAt: expect.any(Date) + }); + expect(database.onConflictDoUpdate).toHaveBeenCalledWith({ + target: discordProfile.userId, + set: { ...assets, syncedAt: expect.any(Date) } + }); + }); + + it("scopes lookups to the requested user", async () => { + const profile = { userId: "authenticated-user", discordUserId: "123" }; + database.limit.mockResolvedValue([profile]); + + expect(await getDiscordProfile("authenticated-user")).toBe(profile); + expect(database.from).toHaveBeenCalledWith(discordProfile); + const condition = new PgDialect().sqlToQuery(database.where.mock.calls[0][0]); + expect(condition.sql).toBe('"discord_profile"."user_id" = $1'); + expect(condition.params).toEqual(["authenticated-user"]); + expect(database.limit).toHaveBeenCalledWith(1); + }); + + it("returns null for users who have not synced yet", async () => { + expect(await getDiscordProfile("new-user")).toBeNull(); + }); +}); diff --git a/src/lib/server/discord-profile.ts b/src/lib/server/discord-profile.ts new file mode 100644 index 000000000..882ddb8a8 --- /dev/null +++ b/src/lib/server/discord-profile.ts @@ -0,0 +1,58 @@ +import { db } from "$lib/server/db"; +import { discordProfile } from "$lib/server/db/schema"; +import { eq } from "drizzle-orm"; +import { z } from "zod"; + +const assetHash = z + .string() + .regex(/^[a-zA-Z0-9_]+$/) + .nullish(); +const discordAssetSchema = z.object({ + id: z.string().regex(/^\d+$/), + username: z.string().min(1).nullish(), + primary_guild: z + .object({ + identity_guild_id: z.string().regex(/^\d+$/).nullable().default(null), + identity_enabled: z.boolean().nullable().default(null), + tag: z.string().nullable().default(null), + badge: assetHash.transform((value) => value ?? null) + }) + .nullish(), + avatar: assetHash, + banner: assetHash, + avatar_decoration: assetHash, + avatar_decoration_data: z.object({ asset: z.string().regex(/^[a-zA-Z0-9_]+$/) }).nullish() +}); + +export function parseDiscordProfile(profile: unknown) { + const result = discordAssetSchema.safeParse(profile); + if (!result.success) return null; + const data = result.data; + return { + discordUserId: data.id, + username: data.username ?? null, + primaryGuild: data.primary_guild ?? null, + avatarHash: data.avatar ?? null, + bannerHash: data.banner ?? null, + // Prefer the current Discord field; explicit null means the decoration was removed. + avatarDecorationHash: + data.avatar_decoration_data === undefined + ? (data.avatar_decoration ?? null) + : (data.avatar_decoration_data?.asset ?? null) + }; +} + +export type DiscordProfileAssets = NonNullable>; + +export async function syncDiscordProfile(userId: string, assets: DiscordProfileAssets) { + const values = { ...assets, syncedAt: new Date() }; + await db + .insert(discordProfile) + .values({ userId, ...values }) + .onConflictDoUpdate({ target: discordProfile.userId, set: values }); +} + +export async function getDiscordProfile(userId: string) { + const [profile] = await db.select().from(discordProfile).where(eq(discordProfile.userId, userId)).limit(1); + return profile ?? null; +} diff --git a/src/lib/server/minecraft-accounts.spec.ts b/src/lib/server/minecraft-accounts.spec.ts new file mode 100644 index 000000000..b5d1e2d42 --- /dev/null +++ b/src/lib/server/minecraft-accounts.spec.ts @@ -0,0 +1,99 @@ +import { afterEach, beforeEach, expect, it, vi } from "vitest"; +import { fetchMcIdProfile, parseMcIdProfile, storeMinecraftAccounts } from "./minecraft-accounts"; + +const database = vi.hoisted(() => ({ + transaction: vi.fn(), + select: vi.fn(), + from: vi.fn(), + where: vi.fn(), + for: vi.fn(), + delete: vi.fn(), + insert: vi.fn(), + values: vi.fn() +})); +vi.mock("$lib/server/db", () => ({ db: database })); + +const link = { id: "local-link", accountId: "provider-subject", userId: "user", providerId: "mc-id" }; +const minecraft = { uuid: "12345678123412341234123456789abc", username: "New_Name", primary: true }; + +beforeEach(() => { + vi.resetAllMocks(); + database.transaction.mockImplementation((handler) => handler(database)); + for (const method of [database.select, database.from, database.where, database.delete, database.insert]) + method.mockReturnValue(database); + database.for.mockResolvedValue([{ id: link.id }]); + database.values.mockResolvedValue(undefined); +}); +afterEach(() => vi.unstubAllGlobals()); + +it("normalizes UUIDs and preserves multiple accounts and primary selection", () => { + expect( + parseMcIdProfile({ + sub: link.accountId, + accounts: [ + { ...minecraft, uuid: "12345678-1234-1234-1234-123456789ABC" }, + { ...minecraft, uuid: "abcdefabcdefabcdefabcdefabcdefab", primary: false } + ] + })?.accounts + ).toEqual([minecraft, { ...minecraft, uuid: "abcdefabcdefabcdefabcdefabcdefab", primary: false }]); +}); + +it("rejects missing account claims while accepting an explicitly empty snapshot", () => { + expect(parseMcIdProfile({ sub: link.accountId })).toBeNull(); + expect(parseMcIdProfile({ sub: link.accountId, accounts: [] })).toEqual({ sub: link.accountId, accounts: [] }); +}); + +it.each([ + [{ ...minecraft }, { ...minecraft, uuid: "12345678-1234-1234-1234-123456789ABC", primary: false }], + [{ ...minecraft }, { ...minecraft, uuid: "abcdefabcdefabcdefabcdefabcdefab" }], + [{ ...minecraft, uuid: "invalid" }], + [{ ...minecraft, username: "../invalid" }] +])("rejects duplicate UUIDs, multiple primary accounts, and invalid identities: %j", (...accounts) => { + expect(parseMcIdProfile({ sub: link.accountId, accounts })).toBeNull(); +}); + +it("replaces usernames and account snapshots inside a transaction", async () => { + await storeMinecraftAccounts(link, { sub: link.accountId, accounts: [minecraft] }); + expect(database.transaction).toHaveBeenCalledOnce(); + expect(database.for).toHaveBeenCalledWith("update"); + expect(database.delete).toHaveBeenCalledOnce(); + expect(database.values).toHaveBeenCalledWith([{ ...minecraft, accountId: link.id, syncedAt: expect.any(Date) }]); + expect(database.delete.mock.invocationCallOrder[0]).toBeLessThan(database.insert.mock.invocationCallOrder[0]); +}); + +it("clears accounts only for an explicit empty snapshot", async () => { + await storeMinecraftAccounts(link, { sub: link.accountId, accounts: [] }); + expect(database.delete).toHaveBeenCalledOnce(); + expect(database.insert).not.toHaveBeenCalled(); +}); + +it("rejects a mismatched provider subject before modifying data", async () => { + await expect(storeMinecraftAccounts(link, { sub: "another-user", accounts: [] })).rejects.toThrow("does not match"); + expect(database.transaction).not.toHaveBeenCalled(); +}); + +it("does not save accounts after the MC-ID link has been removed", async () => { + database.for.mockResolvedValue([]); + await expect(storeMinecraftAccounts(link, { sub: link.accountId, accounts: [minecraft] })).rejects.toThrow( + "no longer linked" + ); + expect(database.delete).not.toHaveBeenCalled(); +}); + +it("fetches userinfo with a bearer token and rejects incomplete successful responses", async () => { + const fetch = vi.fn().mockResolvedValue(Response.json({ sub: link.accountId })); + vi.stubGlobal("fetch", fetch); + await expect(fetchMcIdProfile("test-token")).rejects.toThrow("incomplete"); + expect(fetch).toHaveBeenCalledWith( + "https://mc-id.com/api/auth/oauth2/userinfo", + expect.objectContaining({ + headers: { Authorization: "Bearer test-token" }, + redirect: "error" + }) + ); +}); + +it.each([401, 403])("requires reconnection when userinfo returns %s", async (status) => { + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response(null, { status }))); + expect(await fetchMcIdProfile("test-token")).toBeNull(); +}); diff --git a/src/lib/server/minecraft-accounts.ts b/src/lib/server/minecraft-accounts.ts new file mode 100644 index 000000000..60b59b063 --- /dev/null +++ b/src/lib/server/minecraft-accounts.ts @@ -0,0 +1,104 @@ +import { db } from "$lib/server/db"; +import { account, minecraftAccount } from "$lib/server/db/schema"; +import { and, asc, desc, eq } from "drizzle-orm"; +import { z } from "zod"; + +const minecraftUuid = z + .string() + .regex(/^(?:[a-fA-F0-9]{32}|[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})$/) + .transform((uuid) => uuid.replaceAll("-", "").toLowerCase()); +const mcIdProfileSchema = z.object({ + sub: z.string().min(1), + name: z.string().optional(), + email: z.string().optional(), + email_verified: z.boolean().optional(), + accounts: z + .array( + z.object({ + uuid: minecraftUuid, + username: z + .string() + .min(1) + .max(16) + .regex(/^[a-zA-Z0-9_]+$/), + primary: z.boolean() + }) + ) + .refine( + (accounts) => + new Set(accounts.map((account) => account.uuid)).size === accounts.length && + accounts.filter((account) => account.primary).length <= 1 + ) +}); + +export function parseMcIdProfile(profile: unknown) { + const result = mcIdProfileSchema.safeParse(profile); + return result.success ? result.data : null; +} + +export type McIdProfile = NonNullable>; +export type McIdLink = Pick; + +export async function getMcIdLinks(userId: string) { + return db + .select({ id: account.id, accountId: account.accountId, userId: account.userId, providerId: account.providerId }) + .from(account) + .where(and(eq(account.userId, userId), eq(account.providerId, "mc-id"))); +} + +export async function getStoredMinecraftAccounts(userId: string) { + return db + .select({ + uuid: minecraftAccount.uuid, + username: minecraftAccount.username, + primary: minecraftAccount.primary, + syncedAt: minecraftAccount.syncedAt, + accountId: minecraftAccount.accountId + }) + .from(minecraftAccount) + .innerJoin(account, eq(minecraftAccount.accountId, account.id)) + .where(and(eq(account.userId, userId), eq(account.providerId, "mc-id"))) + .orderBy(desc(minecraftAccount.primary), asc(minecraftAccount.username)); +} + +export async function storeMinecraftAccounts(link: McIdLink, profile: McIdProfile) { + if (link.providerId !== "mc-id" || profile.sub !== link.accountId) + throw new Error("MC-ID identity does not match the linked account."); + await db.transaction(async (tx) => { + // Serialize snapshots and ensure the link still belongs to this user before replacing it. + const [owner] = await tx + .select({ id: account.id }) + .from(account) + .where( + and( + eq(account.id, link.id), + eq(account.userId, link.userId), + eq(account.providerId, "mc-id"), + eq(account.accountId, profile.sub) + ) + ) + .for("update"); + if (!owner) throw new Error("MC-ID account is no longer linked."); + await tx.delete(minecraftAccount).where(eq(minecraftAccount.accountId, link.id)); + if (profile.accounts.length) { + const syncedAt = new Date(); + await tx + .insert(minecraftAccount) + .values(profile.accounts.map((minecraft) => ({ ...minecraft, accountId: link.id, syncedAt }))); + } + }); +} + +export async function fetchMcIdProfile(accessToken: string) { + const response = await fetch("https://mc-id.com/api/auth/oauth2/userinfo", { + headers: { Authorization: `Bearer ${accessToken}` }, + signal: AbortSignal.timeout(10_000), + redirect: "error" + }); + if (response.status === 401 || response.status === 403) return null; + if (!response.ok) throw new Error("MC-ID could not return your Minecraft accounts."); + const profile = parseMcIdProfile(await response.json()); + if (!profile) + throw new Error("MC-ID returned incomplete Minecraft account data. Reconnect MC-ID to grant profile access."); + return profile; +} diff --git a/src/lib/server/patreon-cooldown.spec.ts b/src/lib/server/patreon-cooldown.spec.ts new file mode 100644 index 000000000..a3ceaeb6b --- /dev/null +++ b/src/lib/server/patreon-cooldown.spec.ts @@ -0,0 +1,19 @@ +import { afterEach, expect, it, vi } from "vitest"; +import { claimPatreonVerification, getPatreonVerificationRetryAt } from "./patreon-cooldown"; + +afterEach(() => vi.useRealTimers()); + +it("limits each user independently and permits retries exactly one minute later", () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-10-03T12:00:00Z")); + expect(getPatreonVerificationRetryAt("patron")).toBeNull(); + expect(claimPatreonVerification("patron")).toBe(true); + expect(claimPatreonVerification("patron")).toBe(false); + expect(claimPatreonVerification("another-patron")).toBe(true); + expect(getPatreonVerificationRetryAt("patron")).toEqual(new Date("2026-10-03T12:01:00Z")); + vi.advanceTimersByTime(59_999); + expect(claimPatreonVerification("patron")).toBe(false); + vi.advanceTimersByTime(1); + expect(getPatreonVerificationRetryAt("patron")).toBeNull(); + expect(claimPatreonVerification("patron")).toBe(true); +}); diff --git a/src/lib/server/patreon-cooldown.ts b/src/lib/server/patreon-cooldown.ts new file mode 100644 index 000000000..4f953b88c --- /dev/null +++ b/src/lib/server/patreon-cooldown.ts @@ -0,0 +1,17 @@ +const retryTimes = new Map(); + +// ponytail: per-process limits with O(n) cleanup; use a shared TTL cache for high volume or multiple instances. +export function getPatreonVerificationRetryAt(userId: string) { + const retryAt = retryTimes.get(userId) ?? 0; + return retryAt > Date.now() ? new Date(retryAt) : null; +} + +export function claimPatreonVerification(userId: string) { + const now = Date.now(); + for (const [id, retryAt] of retryTimes) { + if (retryAt <= now) retryTimes.delete(id); + } + if (retryTimes.has(userId)) return false; + retryTimes.set(userId, now + 60_000); + return true; +} diff --git a/src/lib/server/patreon-entitlements.ts b/src/lib/server/patreon-entitlements.ts new file mode 100644 index 000000000..1be251c77 --- /dev/null +++ b/src/lib/server/patreon-entitlements.ts @@ -0,0 +1,38 @@ +export const PatreonEntitlement = { + None: 0, + ColoredHeart: 1, + AnyEmoji: 2 +} as const; + +export type PatreonEntitlement = (typeof PatreonEntitlement)[keyof typeof PatreonEntitlement]; + +export type PatreonCampaignMember = { + id: string; + attributes?: { + campaign_lifetime_support_cents?: number; + currently_entitled_amount_cents?: number; + patron_status?: string | null; + }; + relationships?: { + user?: { data?: { id?: string } }; + }; +}; + +export type PatreonCampaignMembersResponse = { + data: PatreonCampaignMember[]; + meta?: { pagination?: { cursors?: { next?: string | null } } }; +}; + +export function getPatreonEntitlement( + lifetimeSupportCents: number, + coloredHeartCents = 500, + anyEmojiCents = 6000 +): PatreonEntitlement { + if (lifetimeSupportCents >= anyEmojiCents) return PatreonEntitlement.AnyEmoji; + if (lifetimeSupportCents >= coloredHeartCents) return PatreonEntitlement.ColoredHeart; + return PatreonEntitlement.None; +} + +export function findPatreonMembership(response: PatreonCampaignMembersResponse, patreonUserId: string) { + return response.data.find((member) => member.relationships?.user?.data?.id === patreonUserId); +} diff --git a/src/lib/server/patreon.db.spec.ts b/src/lib/server/patreon.db.spec.ts new file mode 100644 index 000000000..211ea0ab0 --- /dev/null +++ b/src/lib/server/patreon.db.spec.ts @@ -0,0 +1,66 @@ +import { randomUUID } from "node:crypto"; +import { describe, expect, it, vi } from "vitest"; + +describe.skipIf(!process.env.DATABASE_URL)("Patreon entitlement persistence", () => { + it("never lowers a previously earned entitlement", async () => { + const [{ db }, { user }, { storePatreonMembership }] = await Promise.all([ + import("$lib/server/db"), + import("$lib/server/db/schema"), + import("./patreon") + ]); + const userId = randomUUID(); + + await db.insert(user).values({ id: userId, name: "Patreon test", email: `${userId}@example.invalid` }); + try { + await storePatreonMembership(userId, randomUUID(), "5264298", { + id: randomUUID(), + attributes: { campaign_lifetime_support_cents: 6000 } + }); + const stored = await storePatreonMembership(userId, randomUUID(), "5264298", { + id: randomUUID(), + attributes: { campaign_lifetime_support_cents: 500 } + }); + + expect(stored.entitlementLevel).toBe(2); + } finally { + const { eq } = await import("drizzle-orm"); + await db.delete(user).where(eq(user.id, userId)); + } + }); + + it("persists rotated creator tokens", async () => { + const [{ db }, { patreonCreatorToken }, { refreshCreatorAccessToken }, { eq }] = await Promise.all([ + import("$lib/server/db"), + import("$lib/server/db/schema"), + import("./patreon"), + import("drizzle-orm") + ]); + const campaignId = randomUUID(); + const fetchMock = vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response(JSON.stringify({ access_token: "new-access", refresh_token: "new-refresh", expires_in: 3600 }), { + status: 200 + }) + ); + const originalClientId = process.env.PATREON_CLIENT_ID; + const originalClientSecret = process.env.PATREON_CLIENT_SECRET; + process.env.PATREON_CLIENT_ID = originalClientId || "test-client"; + process.env.PATREON_CLIENT_SECRET = originalClientSecret || "test-secret"; + + await db.insert(patreonCreatorToken).values({ campaignId, accessToken: "old-access", refreshToken: "old-refresh" }); + try { + expect(await refreshCreatorAccessToken(campaignId, "old-access")).toBe("new-access"); + const [stored] = await db + .select() + .from(patreonCreatorToken) + .where(eq(patreonCreatorToken.campaignId, campaignId)); + expect(stored.refreshToken).toBe("new-refresh"); + } finally { + fetchMock.mockRestore(); + if (originalClientId === undefined) delete process.env.PATREON_CLIENT_ID; + else process.env.PATREON_CLIENT_ID = originalClientId; + if (originalClientSecret === undefined) delete process.env.PATREON_CLIENT_SECRET; + else process.env.PATREON_CLIENT_SECRET = originalClientSecret; + await db.delete(patreonCreatorToken).where(eq(patreonCreatorToken.campaignId, campaignId)); + } + }); +}); diff --git a/src/lib/server/patreon.spec.ts b/src/lib/server/patreon.spec.ts new file mode 100644 index 000000000..a818f3461 --- /dev/null +++ b/src/lib/server/patreon.spec.ts @@ -0,0 +1,50 @@ +import { patreonCreatorToken, patreonMembership } from "$lib/server/db/schema"; +import { describe, expect, it } from "vitest"; +import { + findPatreonMembership, + getPatreonEntitlement, + PatreonEntitlement, + type PatreonCampaignMembersResponse +} from "./patreon-entitlements"; + +it("exports the Patreon schema independently of Better Auth generation", () => { + expect(patreonMembership.entitlementLevel).toBeDefined(); + expect(patreonCreatorToken.refreshToken).toBeDefined(); +}); + +describe("getPatreonEntitlement", () => { + it.each([ + [0, PatreonEntitlement.None], + [499, PatreonEntitlement.None], + [500, PatreonEntitlement.ColoredHeart], + [5999, PatreonEntitlement.ColoredHeart], + [6000, PatreonEntitlement.AnyEmoji] + ])("maps %i cents to entitlement %i", (cents, entitlement) => { + expect(getPatreonEntitlement(cents)).toBe(entitlement); + }); + + it("supports configured thresholds", () => { + expect(getPatreonEntitlement(200, 100, 200)).toBe(PatreonEntitlement.AnyEmoji); + }); +}); + +describe("findPatreonMembership", () => { + it("matches the linked Patreon user in the configured campaign response", () => { + const response = { + data: [ + { + id: "member-1", + relationships: { user: { data: { id: "another-user" } } } + }, + { + id: "member-2", + attributes: { campaign_lifetime_support_cents: 6000 }, + relationships: { user: { data: { id: "patreon-user" } } } + } + ] + } satisfies PatreonCampaignMembersResponse; + + expect(findPatreonMembership(response, "patreon-user")?.id).toBe("member-2"); + expect(findPatreonMembership(response, "missing-user")).toBeUndefined(); + }); +}); diff --git a/src/lib/server/patreon.ts b/src/lib/server/patreon.ts new file mode 100644 index 000000000..97d8768e6 --- /dev/null +++ b/src/lib/server/patreon.ts @@ -0,0 +1,315 @@ +import { env as privateEnv } from "$env/dynamic/private"; +import { env as publicEnv } from "$env/dynamic/public"; +import { db } from "$lib/server/db"; +import { account, patreonCreatorToken, patreonMembership } from "$lib/server/db/schema"; +import { claimPatreonVerification } from "$lib/server/patreon-cooldown"; +import { + findPatreonMembership, + getPatreonEntitlement, + type PatreonCampaignMember, + type PatreonCampaignMembersResponse, + type PatreonEntitlement +} from "$lib/server/patreon-entitlements"; +import { and, eq, sql } from "drizzle-orm"; + +export { getPatreonVerificationRetryAt } from "$lib/server/patreon-cooldown"; + +const PATREON_API_URL = "https://www.patreon.com/api/oauth2/v2"; +const PATREON_TOKEN_URL = "https://www.patreon.com/api/oauth2/token"; +const PATREON_TIMEOUT_MS = 10_000; + +export type PatreonMembershipStatus = { + entitlementLevel: PatreonEntitlement; + lifetimeSupportCents: number; + currentlyEntitledAmountCents: number; + patronStatus: string | null; + verifiedAt: Date; +}; + +export class PatreonVerificationError extends Error { + constructor( + message: string, + readonly code: + | "not_configured" + | "not_linked" + | "not_a_member" + | "already_claimed" + | "missing_scope" + | "rate_limited" + | "unavailable" + ) { + super(message); + this.name = "PatreonVerificationError"; + } +} + +export async function getStoredPatreonMembership(userId: string): Promise { + const [membership] = await db + .select({ + entitlementLevel: patreonMembership.entitlementLevel, + lifetimeSupportCents: patreonMembership.lifetimeSupportCents, + currentlyEntitledAmountCents: patreonMembership.currentlyEntitledAmountCents, + patronStatus: patreonMembership.patronStatus, + verifiedAt: patreonMembership.verifiedAt + }) + .from(patreonMembership) + .where(eq(patreonMembership.userId, userId)) + .limit(1); + + return (membership as PatreonMembershipStatus | undefined) ?? null; +} + +export async function refreshCreatorAccessToken(campaignId: string, staleAccessToken?: string) { + const clientId = privateEnv.PATREON_CLIENT_ID?.trim(); + const clientSecret = privateEnv.PATREON_CLIENT_SECRET?.trim(); + if (!clientId || !clientSecret) { + throw new PatreonVerificationError("Patreon creator access is not configured.", "not_configured"); + } + + const [stored] = await db + .select() + .from(patreonCreatorToken) + .where(eq(patreonCreatorToken.campaignId, campaignId)) + .limit(1); + + if (!stored) throw new PatreonVerificationError("Patreon creator access is not configured.", "not_configured"); + if (staleAccessToken && stored.accessToken !== staleAccessToken) return stored.accessToken; + + let response: Response; + try { + response = await fetch(PATREON_TOKEN_URL, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: new URLSearchParams({ + grant_type: "refresh_token", + refresh_token: stored.refreshToken, + client_id: clientId, + client_secret: clientSecret + }), + signal: AbortSignal.timeout(PATREON_TIMEOUT_MS) + }); + } catch { + throw new PatreonVerificationError("Patreon creator token refresh failed.", "unavailable"); + } + + if (!response.ok) { + throw new PatreonVerificationError("Patreon creator token refresh failed.", "not_configured"); + } + + const tokens = (await response.json()) as { + access_token?: string; + refresh_token?: string; + expires_in?: number; + }; + if (!tokens.access_token || !tokens.refresh_token) { + throw new PatreonVerificationError("Patreon returned invalid creator tokens.", "unavailable"); + } + + const expiresAt = Number.isFinite(tokens.expires_in) ? new Date(Date.now() + Number(tokens.expires_in) * 1000) : null; + await db + .update(patreonCreatorToken) + .set({ + accessToken: tokens.access_token, + refreshToken: tokens.refresh_token, + expiresAt, + updatedAt: new Date() + }) + .where(eq(patreonCreatorToken.campaignId, campaignId)); + + return tokens.access_token; +} + +async function getCreatorAccessToken(campaignId: string) { + const accessToken = privateEnv.PATREON_CREATOR_ACCESS_TOKEN?.trim(); + const refreshToken = privateEnv.PATREON_CREATOR_REFRESH_TOKEN?.trim(); + if (!accessToken || !refreshToken || !privateEnv.PATREON_CLIENT_ID || !privateEnv.PATREON_CLIENT_SECRET) { + throw new PatreonVerificationError("Patreon creator access is not configured.", "not_configured"); + } + + await db.insert(patreonCreatorToken).values({ campaignId, accessToken, refreshToken }).onConflictDoNothing(); + + const [stored] = await db + .select() + .from(patreonCreatorToken) + .where(eq(patreonCreatorToken.campaignId, campaignId)) + .limit(1); + + if (stored.expiresAt && stored.expiresAt.getTime() <= Date.now() + 60_000) { + return refreshCreatorAccessToken(campaignId, stored.accessToken); + } + return stored.accessToken; +} + +async function fetchCampaignMembership(campaignId: string, patreonUserId: string, initialCreatorToken: string) { + let cursor: string | undefined; + let creatorToken = initialCreatorToken; + const signal = AbortSignal.timeout(PATREON_TIMEOUT_MS); + + do { + const url = new URL(`${PATREON_API_URL}/campaigns/${campaignId}/members`); + url.searchParams.set("include", "user"); + url.searchParams.set( + "fields[member]", + "campaign_lifetime_support_cents,currently_entitled_amount_cents,patron_status" + ); + url.searchParams.set("fields[user]", "full_name"); + url.searchParams.set("page[count]", "1000"); + if (cursor) url.searchParams.set("page[cursor]", cursor); + + let response: Response; + try { + response = await fetch(url, { + headers: { Accept: "application/vnd.api+json", Authorization: `Bearer ${creatorToken}` }, + signal + }); + } catch { + throw new PatreonVerificationError("Patreon verification is temporarily unavailable.", "unavailable"); + } + + if (response.status === 401) { + creatorToken = await refreshCreatorAccessToken(campaignId, creatorToken); + try { + response = await fetch(url, { + headers: { Accept: "application/vnd.api+json", Authorization: `Bearer ${creatorToken}` }, + signal + }); + } catch { + throw new PatreonVerificationError("Patreon verification is temporarily unavailable.", "unavailable"); + } + } + + if (response.status === 401 || response.status === 403) { + throw new PatreonVerificationError( + "Patreon creator access is not configured with the campaigns.members permission.", + "missing_scope" + ); + } + if (response.status === 429) { + throw new PatreonVerificationError( + "Patreon is rate limiting verification. Please try again later.", + "rate_limited" + ); + } + if (!response.ok) { + throw new PatreonVerificationError("Patreon verification is temporarily unavailable.", "unavailable"); + } + + const page = (await response.json()) as PatreonCampaignMembersResponse; + const membership = findPatreonMembership(page, patreonUserId); + if (membership) return membership; + cursor = page.meta?.pagination?.cursors?.next ?? undefined; + } while (cursor); + + return undefined; +} + +export async function storePatreonMembership( + userId: string, + patreonUserId: string, + campaignId: string, + membership: PatreonCampaignMember, + coloredHeartCents = Number(publicEnv.PUBLIC_PATREON_COLORED_HEART_CENTS ?? 500), + anyEmojiCents = Number(publicEnv.PUBLIC_PATREON_ANY_EMOJI_CENTS ?? 6000) +): Promise { + const lifetimeSupportCents = membership.attributes?.campaign_lifetime_support_cents ?? 0; + const currentlyEntitledAmountCents = membership.attributes?.currently_entitled_amount_cents ?? 0; + const patronStatus = membership.attributes?.patron_status ?? null; + const entitlementLevel = getPatreonEntitlement(lifetimeSupportCents, coloredHeartCents, anyEmojiCents); + const verifiedAt = new Date(); + + const [stored] = await db + .insert(patreonMembership) + .values({ + id: crypto.randomUUID(), + userId, + patreonUserId, + memberId: membership.id, + campaignId, + lifetimeSupportCents, + currentlyEntitledAmountCents, + patronStatus, + entitlementLevel, + verifiedAt + }) + .onConflictDoUpdate({ + target: patreonMembership.userId, + set: { + patreonUserId, + memberId: membership.id, + campaignId, + lifetimeSupportCents, + currentlyEntitledAmountCents, + patronStatus, + entitlementLevel: sql`greatest(${patreonMembership.entitlementLevel}, ${entitlementLevel})`, + verifiedAt, + updatedAt: verifiedAt + } + }) + .returning({ + entitlementLevel: patreonMembership.entitlementLevel, + lifetimeSupportCents: patreonMembership.lifetimeSupportCents, + currentlyEntitledAmountCents: patreonMembership.currentlyEntitledAmountCents, + patronStatus: patreonMembership.patronStatus, + verifiedAt: patreonMembership.verifiedAt + }); + + return stored as PatreonMembershipStatus; +} + +export async function verifyPatreonMembership(userId: string): Promise { + const campaignId = privateEnv.PATREON_CAMPAIGN_ID?.trim(); + const coloredHeartCents = Number(publicEnv.PUBLIC_PATREON_COLORED_HEART_CENTS ?? 500); + const anyEmojiCents = Number(publicEnv.PUBLIC_PATREON_ANY_EMOJI_CENTS ?? 6000); + if ( + !campaignId || + !Number.isSafeInteger(coloredHeartCents) || + coloredHeartCents < 0 || + !Number.isSafeInteger(anyEmojiCents) || + anyEmojiCents < coloredHeartCents + ) { + throw new PatreonVerificationError("Patreon verification is not configured.", "not_configured"); + } + + const [linkedAccount] = await db + .select({ patreonUserId: account.accountId }) + .from(account) + .where(and(eq(account.userId, userId), eq(account.providerId, "patreon"))) + .limit(1); + + if (!linkedAccount) { + throw new PatreonVerificationError("Link your Patreon account before verifying membership.", "not_linked"); + } + + if (!claimPatreonVerification(userId)) { + throw new PatreonVerificationError("Wait one minute before verifying Patreon again.", "rate_limited"); + } + const creatorToken = await getCreatorAccessToken(campaignId); + const membership = await fetchCampaignMembership(campaignId, linkedAccount.patreonUserId, creatorToken); + if (!membership) { + throw new PatreonVerificationError( + "This Patreon account is not a member of the SkyCrypt campaign.", + "not_a_member" + ); + } + + try { + return await storePatreonMembership( + userId, + linkedAccount.patreonUserId, + campaignId, + membership, + coloredHeartCents, + anyEmojiCents + ); + } catch (error) { + if ( + error && + typeof error === "object" && + "constraint_name" in error && + error.constraint_name === "patreon_membership_memberId_uidx" + ) { + throw new PatreonVerificationError("This Patreon account is already linked to another user.", "already_claimed"); + } + throw error; + } +} diff --git a/src/lib/server/profile-emoji.db.spec.ts b/src/lib/server/profile-emoji.db.spec.ts new file mode 100644 index 000000000..36515eddb --- /dev/null +++ b/src/lib/server/profile-emoji.db.spec.ts @@ -0,0 +1,96 @@ +import { randomUUID } from "node:crypto"; +import { readFileSync } from "node:fs"; +import { eq } from "drizzle-orm"; +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import * as schema from "./db/schema"; + +const database = vi.hoisted(() => ({ db: undefined as unknown })); +vi.mock("$lib/server/db", () => ({ + get db() { + return database.db; + } +})); + +describe.skipIf(!process.env.DATABASE_URL)("Profile emoji grants", () => { + const namespace = `emoji_test_${randomUUID().replaceAll("-", "")}`; + const userId = randomUUID(); + const accountId = randomUUID(); + const uuid = randomUUID().replaceAll("-", ""); + let client: ReturnType; + let admin: ReturnType; + let db: ReturnType>; + + beforeAll(async () => { + admin = postgres(process.env.DATABASE_URL!, { onnotice: () => {} }); + await admin`CREATE SCHEMA ${admin(namespace)}`; + client = postgres(process.env.DATABASE_URL!, { connection: { search_path: namespace }, onnotice: () => {} }); + db = drizzle(client, { schema }); + database.db = db; + // An isolated schema runs the real migrations without changing application data. + for (const migration of [ + "0000_mixed_lord_tyger", + "0001_nosy_zuras", + "0002_lonely_nick_fury", + "0003_mean_tiger_shark", + "0004_swift_shotgun", + "0005_special_medusa", + "0006_striped_caretaker", + "0007_wooden_molly_hayes", + "0008_profile_emoji", + "0009_remove_verification_cooldown_table" + ]) { + const sql = readFileSync(`drizzle/${migration}.sql`, "utf8").replaceAll('"public".', `"${namespace}".`); + for (const statement of sql.split("--> statement-breakpoint")) await client.unsafe(statement); + } + await db.insert(schema.user).values({ id: userId, name: "Emoji test", email: `${userId}@example.invalid` }); + await db + .insert(schema.account) + .values({ id: accountId, userId, accountId: "mc-id-test", providerId: "mc-id", updatedAt: new Date() }); + await db.insert(schema.minecraftAccount).values({ accountId, uuid, username: "EmojiTest", primary: true }); + }); + + afterAll(async () => { + if (client) await client.end(); + if (admin) { + await admin`DROP SCHEMA IF EXISTS ${admin(namespace)} CASCADE`; + await admin.end(); + } + }); + + it("enforces ownership and hearts, preserves gifted permissions, and retains Patreon perks", async () => { + const { getEmojiAccounts, saveProfileEmoji } = await import("./profile-emoji"); + await expect(saveProfileEmoji(userId, uuid, "❤️")).rejects.toMatchObject({ status: 403 }); + await db.insert(schema.profileEmoji).values({ uuid, emoji: "❤️", entitlementLevel: 1 }); + await expect(saveProfileEmoji("another-user", uuid, "💙")).rejects.toMatchObject({ status: 403 }); + await expect(saveProfileEmoji(userId, uuid, "🦆")).rejects.toMatchObject({ status: 403 }); + await saveProfileEmoji(userId, uuid, "💙"); + expect((await getEmojiAccounts(userId))[0]).toMatchObject({ emoji: "💙", entitlementLevel: 1 }); + + // A friend can have any-emoji access without a Patreon membership. + await db.update(schema.profileEmoji).set({ entitlementLevel: 2 }).where(eq(schema.profileEmoji.uuid, uuid)); + await saveProfileEmoji(userId, uuid, "🇨🇿"); + expect((await getEmojiAccounts(userId))[0]).toMatchObject({ emoji: "🇨🇿", entitlementLevel: 2 }); + + await db.delete(schema.profileEmoji).where(eq(schema.profileEmoji.uuid, uuid)); + await db.insert(schema.patreonMembership).values({ + id: randomUUID(), + userId, + patreonUserId: "patron", + memberId: "member", + campaignId: "campaign", + entitlementLevel: 2, + patronStatus: "former_patron" + }); + await saveProfileEmoji(userId, uuid, "👩🏽‍💻"); + await db.delete(schema.patreonMembership).where(eq(schema.patreonMembership.userId, userId)); + await saveProfileEmoji(userId, uuid, "🦆"); + expect((await getEmojiAccounts(userId))[0]).toMatchObject({ emoji: "🦆", entitlementLevel: 2 }); + + await db.delete(schema.account).where(eq(schema.account.id, accountId)); + await expect(saveProfileEmoji(userId, uuid, "💜")).rejects.toMatchObject({ status: 403 }); + expect(await getEmojiAccounts(userId)).toEqual([]); + expect((await db.select().from(schema.profileEmoji))[0]).toMatchObject({ emoji: "🦆", entitlementLevel: 2 }); + }); +}); diff --git a/src/lib/server/profile-emoji.spec.ts b/src/lib/server/profile-emoji.spec.ts new file mode 100644 index 000000000..12a924662 --- /dev/null +++ b/src/lib/server/profile-emoji.spec.ts @@ -0,0 +1,85 @@ +import { beforeEach, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + transaction: vi.fn(), + select: vi.fn(), + insert: vi.fn(), + values: vi.fn(), + upsert: vi.fn() +})); +vi.mock("$lib/server/db", () => ({ db: { transaction: mocks.transaction } })); + +import { saveProfileEmoji } from "./profile-emoji"; + +const uuid = "a".repeat(32); + +beforeEach(() => { + vi.resetAllMocks(); + mocks.transaction.mockImplementation(async (handler) => handler({ select: mocks.select, insert: mocks.insert })); + mocks.select.mockImplementation(() => ({ + from: () => ({ + innerJoin: () => ({ where: () => ({ for: async () => [{ id: "owner" }] }) }), + where: async () => [] + }) + })); + mocks.insert.mockReturnValue({ values: mocks.values }); + mocks.values.mockReturnValue({ onConflictDoUpdate: mocks.upsert }); +}); + +function storedLevels(grant: number | null, membership: number | null) { + for (const level of [grant, membership]) { + mocks.select.mockImplementationOnce(() => ({ + from: () => ({ where: async () => (level === null ? [] : [{ entitlementLevel: level }]) }) + })); + } +} + +it.each([ + ["invalid", "❤️"], + [uuid, "hello"], + [uuid, "🦆🦆"] +])("rejects invalid save input before accessing the database", async (accountUuid, emoji) => { + await expect(saveProfileEmoji("user", accountUuid, emoji)).rejects.toMatchObject({ status: 400 }); + expect(mocks.transaction).not.toHaveBeenCalled(); +}); + +it("rejects an account the current user does not own before looking up perks", async () => { + mocks.select.mockImplementationOnce(() => ({ + from: () => ({ innerJoin: () => ({ where: () => ({ for: async () => [] }) }) }) + })); + await expect(saveProfileEmoji("other-user", uuid, "❤️")).rejects.toMatchObject({ status: 403 }); + expect(mocks.select).toHaveBeenCalledOnce(); + expect(mocks.insert).not.toHaveBeenCalled(); +}); + +it.each([ + [null, null, "❤️", false], + [0, 0, "❤️", false], + [1, null, "🦆", false], + [null, 1, "🦆", false], + [1, 1, "🦆", false], + [null, 3, "🦆", false], + [1, null, " ❤ ", true], + [null, 1, "💙", true], + [2, null, "🦆", true], + [null, 2, "👩🏽‍💻", true], + [2, 0, "🇨🇿", true], + [1, 2, "🦆", true] +])("enforces stored grant %s and membership %s for %s", async (grant, membership, emoji, allowed) => { + const ownerSelect = mocks.select.getMockImplementation()!; + mocks.select.mockImplementationOnce(ownerSelect); + storedLevels(grant, membership); + if (!allowed) { + await expect(saveProfileEmoji("user", uuid, emoji)).rejects.toMatchObject({ status: 403 }); + expect(mocks.insert).not.toHaveBeenCalled(); + return; + } + + await saveProfileEmoji("user", uuid, emoji); + expect(mocks.values).toHaveBeenCalledWith({ + uuid, + emoji: emoji === " ❤ " ? "❤️" : emoji, + entitlementLevel: Math.max(grant ?? 0, membership ?? 0) + }); + expect(mocks.upsert).toHaveBeenCalledOnce(); +}); diff --git a/src/lib/server/profile-emoji.ts b/src/lib/server/profile-emoji.ts new file mode 100644 index 000000000..b410e750b --- /dev/null +++ b/src/lib/server/profile-emoji.ts @@ -0,0 +1,58 @@ +import { db } from "$lib/server/db"; +import { account, minecraftAccount, patreonMembership, profileEmoji } from "$lib/server/db/schema"; +import { canSelectEmoji, profileEmojiSchema, profileUuidSchema } from "$lib/shared/profile-emoji"; +import { error } from "@sveltejs/kit"; +import { and, asc, desc, eq, sql } from "drizzle-orm"; + +export async function getEmojiAccounts(userId: string) { + return db + .selectDistinct({ + uuid: minecraftAccount.uuid, + username: minecraftAccount.username, + primary: minecraftAccount.primary, + emoji: profileEmoji.emoji, + entitlementLevel: sql`greatest(coalesce(${profileEmoji.entitlementLevel}, 0), coalesce(${patreonMembership.entitlementLevel}, 0))` + }) + .from(minecraftAccount) + .innerJoin(account, eq(minecraftAccount.accountId, account.id)) + .leftJoin(profileEmoji, eq(profileEmoji.uuid, minecraftAccount.uuid)) + .leftJoin(patreonMembership, eq(patreonMembership.userId, account.userId)) + .where(and(eq(account.userId, userId), eq(account.providerId, "mc-id"))) + .orderBy(desc(minecraftAccount.primary), asc(minecraftAccount.username)); +} + +export async function saveProfileEmoji(userId: string, uuid: string, emoji: string) { + const parsedUuid = profileUuidSchema.safeParse(uuid); + const parsedEmoji = profileEmojiSchema.safeParse(emoji); + if (!parsedUuid.success || !parsedEmoji.success) error(400, "Choose a valid Minecraft account and a single emoji."); + uuid = parsedUuid.data; + emoji = parsedEmoji.data; + + await db.transaction(async (tx) => { + // Lock the verified link so unlinking/syncing cannot race this ownership check. + const [owner] = await tx + .select({ id: account.id }) + .from(account) + .innerJoin(minecraftAccount, eq(minecraftAccount.accountId, account.id)) + .where(and(eq(account.userId, userId), eq(account.providerId, "mc-id"), eq(minecraftAccount.uuid, uuid))) + .for("update", { of: account }); + if (!owner) error(403, "Link this Minecraft account through MC-ID before changing its emoji."); + + const [grant] = await tx.select().from(profileEmoji).where(eq(profileEmoji.uuid, uuid)); + const [membership] = await tx.select().from(patreonMembership).where(eq(patreonMembership.userId, userId)); + const level = Math.max(grant?.entitlementLevel ?? 0, membership?.entitlementLevel ?? 0); + if (!canSelectEmoji(level, emoji)) error(403, "This account has not unlocked that emoji."); + + await tx + .insert(profileEmoji) + .values({ uuid, emoji, entitlementLevel: level }) + .onConflictDoUpdate({ + target: profileEmoji.uuid, + set: { + emoji, + entitlementLevel: sql`greatest(${profileEmoji.entitlementLevel}, ${level})`, + updatedAt: new Date() + } + }); + }); +} diff --git a/src/lib/shared/api/patreon-generated.ts b/src/lib/shared/api/patreon-generated.ts new file mode 100644 index 000000000..c7a9e5651 --- /dev/null +++ b/src/lib/shared/api/patreon-generated.ts @@ -0,0 +1,3988 @@ +/** + * Generated by orval 🍺 Do not edit manually. Patreon API An unofficial OpenAPI schema for the V2 Patreon API. Made + * based on the official api with https://github.com/ghostrider-05/patreon-api.ts OpenAPI spec version: 2 + */ +/** A patron's shipping address. */ +export interface Address { + /** + * Full recipient name + * + * @nullable + */ + addressee?: string | null; + city?: string; + country?: string; + /** Datetime address was first created */ + created_at?: string; + /** + * First line of street address + * + * @nullable + */ + line_1?: string | null; + /** + * Second line of street address + * + * @nullable + */ + line_2?: string | null; + /** + * Telephone number. Specified for non-US addresses + * + * @nullable + */ + phone_number?: string | null; + /** + * Postal or zip code + * + * @nullable + */ + postal_code?: string | null; + /** + * State or province name + * + * @nullable + */ + state?: string | null; +} + +/** + * Any metadata the third-party app included with this benefit on creation + * + * @nullable + */ +export type BenefitAppMeta = { [key: string]: unknown } | null; + +/** A benefit added to the campaign, which can be added to a tier to be delivered to the patron. */ +export interface Benefit { + /** + * The third-party external ID this reward is associated with + * + * @nullable + */ + app_external_id?: string | null; + /** + * Any metadata the third-party app included with this benefit on creation + * + * @nullable + */ + app_meta?: BenefitAppMeta; + /** + * Type of benefit, such as `custom` for creator-defined benefits + * + * @nullable + */ + benefit_type?: string | null; + /** Datetime this benefit was created */ + created_at?: string; + /** Number of deliverables for this benefit that are due today specifically */ + deliverables_due_today_count?: number; + /** Number of deliverables for this benefit that have been marked complete */ + delivered_deliverables_count?: number; + /** + * Display description + * + * @nullable + */ + description?: string | null; + /** Whether this benefit has been deleted */ + is_deleted?: boolean; + /** Whether this benefit is no longer available to new patrons */ + is_ended?: boolean; + /** Whether this benefit is ready to be fulfilled to patrons */ + is_published?: boolean; + /** + * The next due date (after EOD today) for this benefit + * + * @nullable + */ + next_deliverable_due_date?: string | null; + /** Number of deliverables for this benefit that are due, for all dates */ + not_delivered_deliverables_count?: number; + /** + * A rule type designation, such as `eom_monthly` or `one_time_immediate` + * + * @nullable + */ + rule_type?: string | null; + /** Number of tiers containing this benefit */ + tiers_count?: number; + /** Display title */ + title?: string; +} + +/** The creator's page, and the top-level object for accessing lists of members, tiers, etc */ +export interface Campaign { + /** Datetime that the creator first began the campaign creation process */ + created_at?: string; + /** + * The type of content the creator is creating, as in "{@link Campaign.vanity} is creating + * {@link Campaign.creation_name}" + * + * @nullable + */ + creation_name?: string | null; + /** The campaign's currency */ + currency?: string; + /** + * The ID of the external discord server that is linked to this campaign + * + * @nullable + */ + discord_server_id?: string | null; + /** + * The ID of the Google Analytics tracker that the creator wants metrics to be sent to + * + * @nullable + */ + google_analytics_id?: string | null; + /** Banner image URL for the campaign */ + image_url?: string; + /** Profile image URL for the campaign */ + image_small_url?: string; + /** Whether the campaign charges upfront */ + is_charged_immediately?: boolean; + /** Whether the campaign charges per month */ + is_monthly?: boolean; + /** Whether the creator has marked the campaign as containing nsfw content */ + is_nsfw?: boolean; + /** Whether this user has opted-in to rss feeds */ + has_rss?: boolean; + /** Whether the creator has sent a one-time rss notification email */ + has_sent_rss_notify?: boolean; + /** @nullable */ + main_video_embed?: string | null; + /** @nullable */ + main_video_url?: string | null; + /** The campaign's display name */ + name?: string; + /** + * Pithy one-liner for this campaign, displayed on the creator page + * + * @nullable + */ + one_liner?: string | null; + /** + * The thing which patrons are paying per, as in "{@link Campaign.vanity} is making $1000 per + * {@link Campaign.pay_per_name}" + * + * @nullable + */ + pay_per_name?: string | null; + /** Number of patrons having access to this creator */ + patron_count?: number; + /** Relative (to patreon.com) URL for the pledge checkout flow for this campaign */ + pledge_url?: string; + /** + * Datetime that the creator most recently published (made publicly visible) the campaign. Null when the campaign has + * not been public. + * + * @nullable + */ + published_at?: string | null; + /** + * The url for the rss album artwork + * + * @nullable + */ + rss_artwork_url?: string | null; + /** + * The title of the campaigns rss feed + * + * @nullable + */ + rss_feed_title?: string | null; + /** Whether the campaign's total earnings are shown publicly */ + show_earnings?: boolean; + /** + * The summary of this campaign. Can be viewed in the About section of the campaign + * + * @nullable + */ + summary?: string | null; + /** + * The embed that is shown to patrons after pledging + * + * @nullable + */ + thanks_embed?: string | null; + /** + * The message that is shown to patrons after pledging + * + * @nullable + */ + thanks_msg?: string | null; + /** + * The URL of the video that is shown to patrons after pledging + * + * @nullable + */ + thanks_video_url?: string | null; + /** The url to visit this campaign */ + url?: string; + /** + * The campaign's vanity name + * + * @nullable + */ + vanity?: string | null; +} + +/** A client created by a developer, used for getting OAuth2 access tokens. */ +export interface Client { + /** + * The author name provided during client setup. + * + * @nullable + */ + author_name?: string | null; + category?: string; + /** The client's secret. */ + client_secret?: string; + /** The client's default OAuth scopes for the authorization flow. */ + default_scopes?: string; + /** The description provided during client setup. */ + description?: string; + /** + * The domain provided during client setup. + * + * @nullable + */ + domain?: string | null; + /** + * The URL of the icon used in the OAuth authorization flow. + * + * @nullable + */ + icon_url?: string | null; + /** The name provided during client setup. */ + name?: string; + /** + * The URL of the privacy policy provided during client setup. + * + * @nullable + */ + privacy_policy_url?: string | null; + /** The allowable redirect URIs for the OAuth authorization flow. */ + redirect_uris?: string; + /** The URL of the terms of service provided during client setup. */ + tos_url?: string; + /** The Patreon API version the client is targeting. */ + version?: number; +} + +export type DeliverableDeliveryStatus = (typeof DeliverableDeliveryStatus)[keyof typeof DeliverableDeliveryStatus]; + +export const DeliverableDeliveryStatus = { + delivered: "delivered", + not_delivered: "not_delivered", + wont_deliver: "wont_deliver" +} as const; + +/** The record of whether or not a patron has been delivered the benefit they are owed because of their member tier. */ +export interface Deliverable { + /** + * When the creator marked the deliverable as completed or fulfilled to the patron + * + * @nullable + */ + completed_at?: string | null; + delivery_status?: DeliverableDeliveryStatus; + /** When the deliverable is due to the patron */ + due_at?: string; +} + +export interface Goal { + [key: string]: unknown; +} + +/** An access rule that determines who can view a piece of content. */ +export interface LiveAccessRule { + /** The type of access being granted. */ + access_rule_type?: string; +} + +/** + * A livestream associated with a campaign post. + * + * The Live APIs are early-access, and may be subject to change based on feedback from our partners. + */ +export interface Live { + /** + * The description of the livestream + * + * @nullable + */ + description?: string | null; + /** + * The RTMP URL for streaming + * + * @nullable + */ + rtmp_url?: string | null; + /** + * The scheduled start time of the livestream + * + * @nullable + */ + scheduled_for?: string | null; + /** + * The current state of the livestream + * + * Valid state transitions: + * + * - From pre_live, can only go to live + * - From live, can only go to live_ended + * - From live_ended, no further transitions + */ + state?: string; + /** + * The stream key for RTMP streaming + * + * @nullable + */ + stream_key?: string | null; + /** + * The title of the livestream + * + * @nullable + */ + title?: string | null; +} + +/** The resized image URLs for this media. Valid for 2 weeks. */ +export type MediaImageUrls = { [key: string]: unknown }; + +/** + * Metadata related to the file + * + * @nullable + */ +export type MediaMetadata = { [key: string]: unknown } | null; + +/** All the parameters that have to be added to the upload form request */ +export type MediaUploadParameters = { [key: string]: unknown }; + +/** A file uploaded to patreon.com, usually an image. */ +export interface Media { + /** When the file was created */ + created_at?: string; + /** The URL to download this media. Valid for 24 hours. */ + download_url?: string; + /** File name. */ + file_name?: string; + /** The resized image URLs for this media. Valid for 2 weeks. */ + image_urls?: MediaImageUrls; + /** + * Metadata related to the file + * + * @nullable + */ + metadata?: MediaMetadata; + /** Mimetype of uploaded file, eg: "application/jpeg" */ + mimetype?: string; + /** Ownership id (See also {@link Media.owner_type}) */ + owner_id?: string; + /** Ownership relationship type for multi-relationship medias */ + owner_relationship?: string; + /** Type of the resource that owns the file */ + owner_type?: string; + size_bytes?: number; + state?: string; + /** When the upload URL expires */ + upload_expires_at?: string; + /** All the parameters that have to be added to the upload form request */ + upload_parameters?: MediaUploadParameters; + /** The URL to perform a POST request to in order to upload the media file */ + upload_url?: string; +} + +/** + * The result of the last attempted charge. The only successful status is `'Paid'`. `null` if never charged + * + * Note: this will likely be either `'Paid'` or `'Pending'` + * + * @nullable + */ +export type MemberLastChargeStatus = (typeof MemberLastChargeStatus)[keyof typeof MemberLastChargeStatus] | null; + +export const MemberLastChargeStatus = { + Paid: "Paid", + Declined: "Declined", + Deleted: "Deleted", + Pending: "Pending", + Refunded: "Refunded", + Refunded_by_Patreon: "Refunded by Patreon", + Partially_Refunded: "Partially Refunded", + Fraud: "Fraud", + Free_Trial: "Free Trial", + Other: "Other" +} as const; + +/** + * A `null` value indicates the member has never pledged + * + * @nullable + */ +export type MemberPatronStatus = (typeof MemberPatronStatus)[keyof typeof MemberPatronStatus] | null; + +export const MemberPatronStatus = { + active_patron: "active_patron", + declined_patron: "declined_patron", + former_patron: "former_patron" +} as const; + +/** The record of a user's membership to a campaign. Remains consistent across months of pledging. */ +export interface Member { + /** The total amount that the member has ever paid to the campaign in the campaign's currency. 0 if never paid */ + campaign_lifetime_support_cents?: number; + /** + * The amount in cents that the member is entitled to. This includes a current pledge, or payment that covers the + * current payment period + */ + currently_entitled_amount_cents?: number; + /** The member's email address. Requires the campaigns.members[email] scope */ + email?: string; + /** Full name of the member user */ + full_name?: string; + /** + * The user is not a pledging patron but has subscribed to updates about public posts. This will always be false, + * following has been replaced by free membership. + */ + is_follower?: boolean; + /** The user is in a free trial period. */ + is_free_trial?: boolean; + /** The user's membership is from a free gift */ + is_gifted?: boolean; + /** + * Datetime of last attempted charge. `null` if never charged + * + * @nullable + */ + last_charge_date?: string | null; + /** + * The result of the last attempted charge. The only successful status is `'Paid'`. `null` if never charged + * + * Note: this will likely be either `'Paid'` or `'Pending'` + * + * @nullable + */ + last_charge_status?: MemberLastChargeStatus; + /** + * The total amount that the member has ever paid to the campaign in the campaign's currency. 0 if never paid. + * + * Use {@link campaign_lifetime_support_cents}. + */ + lifetime_support_cents?: number; + /** + * Datetime of next charge. `null` if annual pledge downgrade + * + * @nullable + */ + next_charge_date?: string | null; + /** The creator's notes on the member */ + note?: string; + /** + * A `null` value indicates the member has never pledged + * + * @nullable + */ + patron_status?: MemberPatronStatus; + /** + * Number of months between charges + * + * Note: this will be `1` if Campaign.is_monthly is `true` + * + * @nullable + */ + pledge_cadence?: number | null; + /** + * Datetime of beginning of most recent pledge chainfrom this member to the campaign. Pledge updates do not change + * this value + * + * @nullable + */ + pledge_relationship_start?: string | null; + /** The amount in cents the user will pay at the next pay cycle */ + will_pay_amount_cents?: number; +} + +/** + * An object containing embed data if media is embedded in the post + * + * @nullable + */ +export type PostEmbedData = { [key: string]: unknown } | null; + +/** Content posted by a creator on a campaign page. */ +export interface Post { + /** + * Platform app id + * + * @nullable + */ + app_id?: number | null; + /** + * Processing status of the post + * + * @nullable + */ + app_status?: string | null; + /** + * Post content, may include html tags + * + * @nullable + */ + content?: string | null; + /** + * An object containing embed data if media is embedded in the post + * + * @nullable + */ + embed_data?: PostEmbedData; + /** + * Embed media url + * + * @nullable + */ + embed_url?: string | null; + /** + * Whether the post incurs a bill as part of a pay-per-post campaign + * + * @nullable + */ + is_paid?: boolean | null; + /** + * - `true` if the post is viewable by anyone + * - `false` if only patrons (or a subset of patrons) can view + * + * @nullable + */ + is_public?: boolean | null; + /** + * Datetime that the creator most recently published (made publicly visible) the post + * + * @nullable + */ + published_at?: string | null; + /** + * The tier ids that the post is locked for if only patrons (or a subset of patrons) can view. + * + * @nullable + */ + tiers?: string[] | null; + /** @nullable */ + title?: string | null; + /** A URL to access this post on patreon.com */ + url?: string; +} + +/** The payment status of the pledge */ +export type PledgeEventPledgePaymentStatus = + (typeof PledgeEventPledgePaymentStatus)[keyof typeof PledgeEventPledgePaymentStatus]; + +export const PledgeEventPledgePaymentStatus = { + queued: "queued", + pending: "pending", + valid: "valid", + declined: "declined", + fraud: "fraud", + disabled: "disabled" +} as const; + +/** Status of underlying payment */ +export type PledgeEventPaymentStatus = (typeof PledgeEventPaymentStatus)[keyof typeof PledgeEventPaymentStatus]; + +export const PledgeEventPaymentStatus = { + Paid: "Paid", + Declined: "Declined", + Deleted: "Deleted", + Pending: "Pending", + Refunded: "Refunded", + Refunded_by_Patreon: "Refunded by Patreon", + Partially_Refunded: "Partially Refunded", + Fraud: "Fraud", + Free_Trial: "Free Trial", + Other: "Other" +} as const; + +/** Event type. */ +export type PledgeEventType = (typeof PledgeEventType)[keyof typeof PledgeEventType]; + +export const PledgeEventType = { + pledge_start: "pledge_start", + pledge_upgrade: "pledge_upgrade", + pledge_downgrade: "pledge_downgrade", + pledge_delete: "pledge_delete", + subscription: "subscription" +} as const; + +/** The record of a pledging action taken by the user, or that action's failure. */ +export interface PledgeEvent { + /** Amount (in the currency in which the patron paid) of the underlying event */ + amount_cents?: number; + /** ISO code of the currency of the event */ + currency_code?: string; + /** The date which this event occurred */ + date?: string; + /** The payment status of the pledge */ + pledge_payment_status?: PledgeEventPledgePaymentStatus; + /** Status of underlying payment */ + payment_status?: PledgeEventPaymentStatus; + /** + * Id of the tier associated with the pledge + * + * @nullable + */ + tier_id?: string | null; + /** + * Title of the reward tier associated with the pledge + * + * @nullable + */ + tier_title?: string | null; + /** Event type. */ + type?: PledgeEventType; +} + +/** A membership level on a campaign, which can have benefits attached to it. */ +export interface Tier { + /** Monetary amount associated with this tier (in U.S. cents) */ + amount_cents?: number; + /** Datetime this tier was created */ + created_at?: string; + /** Tier display description */ + description?: string; + /** + * The discord role IDs granted by this tier + * + * @nullable + */ + discord_role_ids?: string[] | null; + /** Datetime tier was last modified */ + edited_at?: string; + /** + * Full qualified image URL associated with this tier + * + * @nullable + */ + image_url?: string | null; + /** Number of patrons currently registered for this tier */ + patron_count?: number; + /** Number of posts published to this tier */ + post_count?: number; + /** Whether the tier is currently published */ + published?: boolean; + /** + * Datetime this tier was last published + * + * @nullable + */ + published_at?: string | null; + /** + * Remaining number of patrons who may subscribe, if there is a {@link Tier.user_limit} + * + * @nullable + */ + remaining?: number | null; + /** Whether this tier requires a shipping address from patrons */ + requires_shipping?: boolean; + /** Tier display title */ + title?: string; + /** + * Datetime tier was unpublished, while applicable + * + * @nullable + */ + unpublished_at?: string | null; + /** Fully qualified URL associated with this tier */ + url?: string; + /** + * Maximum number of patrons this tier is limited to, if applicable + * + * @nullable + */ + user_limit?: number | null; +} + +/** + * Mapping from user's connected app names to external user id on the respective app + * + * NOTE: since the documentation is `object`, this can change without notice. For a more accurate representation use the + * following type using TypeScript module augmentation: + * + * ```ts + * import "patreon-api.ts"; + * + * declare module "patreon-api.ts" { + * interface CustomTypeOptions { + * social_connections: Record; + * } + * } + * ``` + */ +export type UserSocialConnections = { [key: string]: unknown }; + +/** The Patreon user, which can be both patron and creator. */ +export interface User { + /** + * The user's about text, which appears on their profile + * + * @nullable + */ + about?: string | null; + /** + * Whether this user can view nsfw content + * + * @nullable + */ + can_see_nsfw?: boolean | null; + /** Datetime of this user's account creation */ + created?: string; + /** The user's email address. Requires certain scopes to access. See the scopes section of the documentation */ + email?: string; + /** + * First name. + * + * @nullable + */ + first_name?: string | null; + /** Combined first and last name */ + full_name?: string; + /** + * Whether the user has chosen to keep private which creators they pledge to + * + * @nullable + */ + hide_pledges?: boolean | null; + /** The user's profile picture URL, scaled to width 400px */ + image_url?: string; + /** Whether this user has an active campaign. */ + is_creator?: boolean; + /** Whether the user has confirmed their email */ + is_email_verified?: boolean; + /** + * Last name. + * + * @nullable + */ + last_name?: string | null; + /** How many posts this user has liked */ + like_count?: number; + /** + * Mapping from user's connected app names to external user id on the respective app + * + * NOTE: since the documentation is `object`, this can change without notice. For a more accurate representation use + * the following type using TypeScript module augmentation: + * + * ```ts + * import "patreon-api.ts"; + * + * declare module "patreon-api.ts" { + * interface CustomTypeOptions { + * social_connections: Record; + * } + * } + * ``` + */ + social_connections?: UserSocialConnections; + /** The user's profile picture URL, scaled to a square of size 100x100px */ + thumb_url?: string; + /** URL of this user's creator or patron profile */ + url?: string; + /** + * The public "username" of the user. patreon.com/ goes to this user's creator page. Non-creator users might not have + * a vanity. + * + * @nullable + */ + vanity?: string | null; +} + +/** Webhooks are fired based on events happening on a particular campaign. */ +export interface Webhook { + /** Last date that the webhook was attempted or used. */ + last_attempted_at?: string; + /** Number of times the webhook has failed consecutively, when in an error state. */ + num_consecutive_times_failed?: number; + /** + * `true` if the webhook is paused as a result of repeated failed attempts to post to uri. Set to `false` to attempt + * to re-enable a previously failing webhook. + */ + paused?: boolean; + /** Secret used to sign your webhook message body, so you can validate authenticity upon receipt. */ + secret?: string; + /** List of events that will trigger this webhook. */ + triggers?: string[]; + /** Fully qualified uri where webhook will be sent */ + uri?: string; +} + +export type WebhookTriggerItem = (typeof WebhookTriggerItem)[keyof typeof WebhookTriggerItem]; + +export const WebhookTriggerItem = { + "members:create": "members:create", + "members:update": "members:update", + "members:delete": "members:delete", + "members:pledge:create": "members:pledge:create", + "members:pledge:update": "members:pledge:update", + "members:pledge:delete": "members:pledge:delete", + "posts:publish": "posts:publish", + "posts:update": "posts:update", + "posts:delete": "posts:delete" +} as const; + +export type WebhookTrigger = WebhookTriggerItem[]; + +export interface JSONAPIError { + code?: number; + code_name?: string; + detail?: string; + id?: string; + status?: string; + title?: string; +} + +export type JSONAPIRatelimitError = JSONAPIError & { + retry_after_seconds?: number; +}; + +export type JSONAPIResourceType = (typeof JSONAPIResourceType)[keyof typeof JSONAPIResourceType]; + +export const JSONAPIResourceType = { + address: "address", + benefit: "benefit", + campaign: "campaign", + client: "client", + deliverable: "deliverable", + goal: "goal", + "live-access-rule": "live-access-rule", + live: "live", + media: "media", + member: "member", + post: "post", + "pledge-event": "pledge-event", + tier: "tier", + user: "user", + webhook: "webhook" +} as const; + +export interface JSONAPIResource { + type: JSONAPIResourceType; + readonly id: string; +} + +export interface JSONAPILinksRelated { + related: string; +} + +export interface JSONAPIResponseLinks { + self: string; +} + +export type JSONAPIResponseMetaMetaPaginationCursors = { + /** @nullable */ + next?: string | null; +}; + +export type JSONAPIResponseMetaMetaPagination = { + cursors: JSONAPIResponseMetaMetaPaginationCursors; +}; + +export type JSONAPIResponseMetaMeta = { + total: number; + pagination: JSONAPIResponseMetaMetaPagination; +}; + +export interface JSONAPIResponseMeta { + meta: JSONAPIResponseMetaMeta; +} + +/** A 204 response with no response body */ +export type N204Response = void; + +/** Bad Request: Something was wrong with your request (syntax, size too large, etc.) */ +export type N400Response = JSONAPIError[]; + +/** Unauthorized: Authentication failed (bad API key, invalid OAuth token, incorrect scopes, etc.) */ +export type N401Response = JSONAPIError[]; + +/** Forbidden: The requested is hidden for administrators only. */ +export type N403Response = JSONAPIError[]; + +/** Not Found: The specified resource could not be found. */ +export type N404Response = JSONAPIError[]; + +/** Method Not Allowed: You tried to access a resource with an invalid method. */ +export type N405Response = JSONAPIError[]; + +/** Not Acceptable: You requested a format that isn't json. */ +export type N406Response = JSONAPIError[]; + +/** Gone: The resource requested has been removed from our servers. */ +export type N410Response = JSONAPIError[]; + +/** Too Many Requests: Slow down! */ +export type N429Response = JSONAPIRatelimitError[]; + +/** Internal Server Error: Our server ran into a problem while processing this request. Please try again later. */ +export type N500Response = JSONAPIError[]; + +/** Service Unavailable: We're temporarily offline for maintenance. Please try again later. */ +export type N503Response = JSONAPIError[]; + +export type UserResponseResponseDataType = + (typeof UserResponseResponseDataType)[keyof typeof UserResponseResponseDataType]; + +export const UserResponseResponseDataType = { + user: "user" +} as const; + +export type UserResponseResponseDataRelationshipsCampaignDataType = + (typeof UserResponseResponseDataRelationshipsCampaignDataType)[keyof typeof UserResponseResponseDataRelationshipsCampaignDataType]; + +export const UserResponseResponseDataRelationshipsCampaignDataType = { + campaign: "campaign" +} as const; + +export type UserResponseResponseDataRelationshipsCampaignData = { + id: string; + type: UserResponseResponseDataRelationshipsCampaignDataType; +}; + +export type UserResponseResponseDataRelationshipsCampaignLinks = { + related?: string; +}; + +export type UserResponseResponseDataRelationshipsCampaign = { + data: UserResponseResponseDataRelationshipsCampaignData; + links?: UserResponseResponseDataRelationshipsCampaignLinks; +}; + +export type UserResponseResponseDataRelationshipsMembershipsDataItemType = + (typeof UserResponseResponseDataRelationshipsMembershipsDataItemType)[keyof typeof UserResponseResponseDataRelationshipsMembershipsDataItemType]; + +export const UserResponseResponseDataRelationshipsMembershipsDataItemType = { + member: "member" +} as const; + +export type UserResponseResponseDataRelationshipsMembershipsDataItem = { + id: string; + type: UserResponseResponseDataRelationshipsMembershipsDataItemType; +}; + +export type UserResponseResponseDataRelationshipsMemberships = { + data: UserResponseResponseDataRelationshipsMembershipsDataItem[]; +}; + +export type UserResponseResponseDataRelationships = { + campaign?: UserResponseResponseDataRelationshipsCampaign; + memberships?: UserResponseResponseDataRelationshipsMemberships; +}; + +export type UserResponseResponseData = { + id: string; + type: UserResponseResponseDataType; + attributes?: User; + relationships?: UserResponseResponseDataRelationships; +}; + +export type UserResponseResponseIncludedItem = + | { + id: string; + type: "campaign"; + attributes: Campaign; + } + | { + id: string; + type: "member"; + attributes: Member; + }; + +export type UserResponseResponse = { + data: UserResponseResponseData; + included?: UserResponseResponseIncludedItem[]; + links: JSONAPIResponseLinks; +}; + +export type CampaignResponseResponseDataType = + (typeof CampaignResponseResponseDataType)[keyof typeof CampaignResponseResponseDataType]; + +export const CampaignResponseResponseDataType = { + campaign: "campaign" +} as const; + +export type CampaignResponseResponseDataRelationshipsBenefitsDataItemType = + (typeof CampaignResponseResponseDataRelationshipsBenefitsDataItemType)[keyof typeof CampaignResponseResponseDataRelationshipsBenefitsDataItemType]; + +export const CampaignResponseResponseDataRelationshipsBenefitsDataItemType = { + benefit: "benefit" +} as const; + +export type CampaignResponseResponseDataRelationshipsBenefitsDataItem = { + id: string; + type: CampaignResponseResponseDataRelationshipsBenefitsDataItemType; +}; + +export type CampaignResponseResponseDataRelationshipsBenefits = { + data: CampaignResponseResponseDataRelationshipsBenefitsDataItem[]; +}; + +export type CampaignResponseResponseDataRelationshipsCreatorDataType = + (typeof CampaignResponseResponseDataRelationshipsCreatorDataType)[keyof typeof CampaignResponseResponseDataRelationshipsCreatorDataType]; + +export const CampaignResponseResponseDataRelationshipsCreatorDataType = { + user: "user" +} as const; + +export type CampaignResponseResponseDataRelationshipsCreatorData = { + id: string; + type: CampaignResponseResponseDataRelationshipsCreatorDataType; +}; + +export type CampaignResponseResponseDataRelationshipsCreatorLinks = { + related?: string; +}; + +export type CampaignResponseResponseDataRelationshipsCreator = { + data: CampaignResponseResponseDataRelationshipsCreatorData; + links?: CampaignResponseResponseDataRelationshipsCreatorLinks; +}; + +export type CampaignResponseResponseDataRelationshipsGoalsDataItemType = + (typeof CampaignResponseResponseDataRelationshipsGoalsDataItemType)[keyof typeof CampaignResponseResponseDataRelationshipsGoalsDataItemType]; + +export const CampaignResponseResponseDataRelationshipsGoalsDataItemType = { + goal: "goal" +} as const; + +export type CampaignResponseResponseDataRelationshipsGoalsDataItem = { + id: string; + type: CampaignResponseResponseDataRelationshipsGoalsDataItemType; +}; + +export type CampaignResponseResponseDataRelationshipsGoals = { + data: CampaignResponseResponseDataRelationshipsGoalsDataItem[]; +}; + +export type CampaignResponseResponseDataRelationshipsLiveAccessRulesDataItemType = + (typeof CampaignResponseResponseDataRelationshipsLiveAccessRulesDataItemType)[keyof typeof CampaignResponseResponseDataRelationshipsLiveAccessRulesDataItemType]; + +export const CampaignResponseResponseDataRelationshipsLiveAccessRulesDataItemType = { + "live-access-rule": "live-access-rule" +} as const; + +export type CampaignResponseResponseDataRelationshipsLiveAccessRulesDataItem = { + id: string; + type: CampaignResponseResponseDataRelationshipsLiveAccessRulesDataItemType; +}; + +export type CampaignResponseResponseDataRelationshipsLiveAccessRules = { + data: CampaignResponseResponseDataRelationshipsLiveAccessRulesDataItem[]; +}; + +export type CampaignResponseResponseDataRelationshipsTiersDataItemType = + (typeof CampaignResponseResponseDataRelationshipsTiersDataItemType)[keyof typeof CampaignResponseResponseDataRelationshipsTiersDataItemType]; + +export const CampaignResponseResponseDataRelationshipsTiersDataItemType = { + tier: "tier" +} as const; + +export type CampaignResponseResponseDataRelationshipsTiersDataItem = { + id: string; + type: CampaignResponseResponseDataRelationshipsTiersDataItemType; +}; + +export type CampaignResponseResponseDataRelationshipsTiers = { + data: CampaignResponseResponseDataRelationshipsTiersDataItem[]; +}; + +export type CampaignResponseResponseDataRelationships = { + benefits?: CampaignResponseResponseDataRelationshipsBenefits; + creator?: CampaignResponseResponseDataRelationshipsCreator; + goals?: CampaignResponseResponseDataRelationshipsGoals; + live_access_rules?: CampaignResponseResponseDataRelationshipsLiveAccessRules; + tiers?: CampaignResponseResponseDataRelationshipsTiers; +}; + +export type CampaignResponseResponseData = { + id: string; + type: CampaignResponseResponseDataType; + attributes?: Campaign; + relationships?: CampaignResponseResponseDataRelationships; +}; + +export type CampaignResponseResponseIncludedItem = + | { + id: string; + type: "benefit"; + attributes: Benefit; + } + | { + id: string; + type: "user"; + attributes: User; + } + | { + id: string; + type: "goal"; + attributes: Goal; + } + | { + id: string; + type: "live-access-rule"; + attributes: LiveAccessRule; + } + | { + id: string; + type: "tier"; + attributes: Tier; + }; + +export type CampaignResponseResponse = { + data: CampaignResponseResponseData; + included?: CampaignResponseResponseIncludedItem[]; + links: JSONAPIResponseLinks; +}; + +export type CampaignsResponseResponseDataItemType = + (typeof CampaignsResponseResponseDataItemType)[keyof typeof CampaignsResponseResponseDataItemType]; + +export const CampaignsResponseResponseDataItemType = { + campaign: "campaign" +} as const; + +export type CampaignsResponseResponseDataItemRelationshipsBenefitsDataItemType = + (typeof CampaignsResponseResponseDataItemRelationshipsBenefitsDataItemType)[keyof typeof CampaignsResponseResponseDataItemRelationshipsBenefitsDataItemType]; + +export const CampaignsResponseResponseDataItemRelationshipsBenefitsDataItemType = { + benefit: "benefit" +} as const; + +export type CampaignsResponseResponseDataItemRelationshipsBenefitsDataItem = { + id: string; + type: CampaignsResponseResponseDataItemRelationshipsBenefitsDataItemType; +}; + +export type CampaignsResponseResponseDataItemRelationshipsBenefits = { + data: CampaignsResponseResponseDataItemRelationshipsBenefitsDataItem[]; +}; + +export type CampaignsResponseResponseDataItemRelationshipsCreatorDataType = + (typeof CampaignsResponseResponseDataItemRelationshipsCreatorDataType)[keyof typeof CampaignsResponseResponseDataItemRelationshipsCreatorDataType]; + +export const CampaignsResponseResponseDataItemRelationshipsCreatorDataType = { + user: "user" +} as const; + +export type CampaignsResponseResponseDataItemRelationshipsCreatorData = { + id: string; + type: CampaignsResponseResponseDataItemRelationshipsCreatorDataType; +}; + +export type CampaignsResponseResponseDataItemRelationshipsCreatorLinks = { + related?: string; +}; + +export type CampaignsResponseResponseDataItemRelationshipsCreator = { + data: CampaignsResponseResponseDataItemRelationshipsCreatorData; + links?: CampaignsResponseResponseDataItemRelationshipsCreatorLinks; +}; + +export type CampaignsResponseResponseDataItemRelationshipsGoalsDataItemType = + (typeof CampaignsResponseResponseDataItemRelationshipsGoalsDataItemType)[keyof typeof CampaignsResponseResponseDataItemRelationshipsGoalsDataItemType]; + +export const CampaignsResponseResponseDataItemRelationshipsGoalsDataItemType = { + goal: "goal" +} as const; + +export type CampaignsResponseResponseDataItemRelationshipsGoalsDataItem = { + id: string; + type: CampaignsResponseResponseDataItemRelationshipsGoalsDataItemType; +}; + +export type CampaignsResponseResponseDataItemRelationshipsGoals = { + data: CampaignsResponseResponseDataItemRelationshipsGoalsDataItem[]; +}; + +export type CampaignsResponseResponseDataItemRelationshipsLiveAccessRulesDataItemType = + (typeof CampaignsResponseResponseDataItemRelationshipsLiveAccessRulesDataItemType)[keyof typeof CampaignsResponseResponseDataItemRelationshipsLiveAccessRulesDataItemType]; + +export const CampaignsResponseResponseDataItemRelationshipsLiveAccessRulesDataItemType = { + "live-access-rule": "live-access-rule" +} as const; + +export type CampaignsResponseResponseDataItemRelationshipsLiveAccessRulesDataItem = { + id: string; + type: CampaignsResponseResponseDataItemRelationshipsLiveAccessRulesDataItemType; +}; + +export type CampaignsResponseResponseDataItemRelationshipsLiveAccessRules = { + data: CampaignsResponseResponseDataItemRelationshipsLiveAccessRulesDataItem[]; +}; + +export type CampaignsResponseResponseDataItemRelationshipsTiersDataItemType = + (typeof CampaignsResponseResponseDataItemRelationshipsTiersDataItemType)[keyof typeof CampaignsResponseResponseDataItemRelationshipsTiersDataItemType]; + +export const CampaignsResponseResponseDataItemRelationshipsTiersDataItemType = { + tier: "tier" +} as const; + +export type CampaignsResponseResponseDataItemRelationshipsTiersDataItem = { + id: string; + type: CampaignsResponseResponseDataItemRelationshipsTiersDataItemType; +}; + +export type CampaignsResponseResponseDataItemRelationshipsTiers = { + data: CampaignsResponseResponseDataItemRelationshipsTiersDataItem[]; +}; + +export type CampaignsResponseResponseDataItemRelationships = { + benefits?: CampaignsResponseResponseDataItemRelationshipsBenefits; + creator?: CampaignsResponseResponseDataItemRelationshipsCreator; + goals?: CampaignsResponseResponseDataItemRelationshipsGoals; + live_access_rules?: CampaignsResponseResponseDataItemRelationshipsLiveAccessRules; + tiers?: CampaignsResponseResponseDataItemRelationshipsTiers; +}; + +export type CampaignsResponseResponseDataItem = { + id: string; + type: CampaignsResponseResponseDataItemType; + attributes?: Campaign; + relationships?: CampaignsResponseResponseDataItemRelationships; +}; + +export type CampaignsResponseResponseIncludedItem = + | { + id: string; + type: "benefit"; + attributes: Benefit; + } + | { + id: string; + type: "user"; + attributes: User; + } + | { + id: string; + type: "goal"; + attributes: Goal; + } + | { + id: string; + type: "live-access-rule"; + attributes: LiveAccessRule; + } + | { + id: string; + type: "tier"; + attributes: Tier; + }; + +export type CampaignsResponseResponse = { + data: CampaignsResponseResponseDataItem[]; + included?: CampaignsResponseResponseIncludedItem[]; + meta: JSONAPIResponseMeta; +}; + +export type MembersResponseResponseDataItemType = + (typeof MembersResponseResponseDataItemType)[keyof typeof MembersResponseResponseDataItemType]; + +export const MembersResponseResponseDataItemType = { + member: "member" +} as const; + +export type MembersResponseResponseDataItemRelationshipsAddressDataType = + (typeof MembersResponseResponseDataItemRelationshipsAddressDataType)[keyof typeof MembersResponseResponseDataItemRelationshipsAddressDataType]; + +export const MembersResponseResponseDataItemRelationshipsAddressDataType = { + address: "address" +} as const; + +export type MembersResponseResponseDataItemRelationshipsAddressData = { + id: string; + type: MembersResponseResponseDataItemRelationshipsAddressDataType; +}; + +export type MembersResponseResponseDataItemRelationshipsAddressLinks = { + related?: string; +}; + +export type MembersResponseResponseDataItemRelationshipsAddress = { + data: MembersResponseResponseDataItemRelationshipsAddressData; + links?: MembersResponseResponseDataItemRelationshipsAddressLinks; +}; + +export type MembersResponseResponseDataItemRelationshipsCampaignDataType = + (typeof MembersResponseResponseDataItemRelationshipsCampaignDataType)[keyof typeof MembersResponseResponseDataItemRelationshipsCampaignDataType]; + +export const MembersResponseResponseDataItemRelationshipsCampaignDataType = { + campaign: "campaign" +} as const; + +export type MembersResponseResponseDataItemRelationshipsCampaignData = { + id: string; + type: MembersResponseResponseDataItemRelationshipsCampaignDataType; +}; + +export type MembersResponseResponseDataItemRelationshipsCampaignLinks = { + related?: string; +}; + +export type MembersResponseResponseDataItemRelationshipsCampaign = { + data: MembersResponseResponseDataItemRelationshipsCampaignData; + links?: MembersResponseResponseDataItemRelationshipsCampaignLinks; +}; + +export type MembersResponseResponseDataItemRelationshipsCurrentlyEntitledTiersDataItemType = + (typeof MembersResponseResponseDataItemRelationshipsCurrentlyEntitledTiersDataItemType)[keyof typeof MembersResponseResponseDataItemRelationshipsCurrentlyEntitledTiersDataItemType]; + +export const MembersResponseResponseDataItemRelationshipsCurrentlyEntitledTiersDataItemType = { + tier: "tier" +} as const; + +export type MembersResponseResponseDataItemRelationshipsCurrentlyEntitledTiersDataItem = { + id: string; + type: MembersResponseResponseDataItemRelationshipsCurrentlyEntitledTiersDataItemType; +}; + +export type MembersResponseResponseDataItemRelationshipsCurrentlyEntitledTiers = { + data: MembersResponseResponseDataItemRelationshipsCurrentlyEntitledTiersDataItem[]; +}; + +export type MembersResponseResponseDataItemRelationshipsPledgeHistoryDataItemType = + (typeof MembersResponseResponseDataItemRelationshipsPledgeHistoryDataItemType)[keyof typeof MembersResponseResponseDataItemRelationshipsPledgeHistoryDataItemType]; + +export const MembersResponseResponseDataItemRelationshipsPledgeHistoryDataItemType = { + "pledge-event": "pledge-event" +} as const; + +export type MembersResponseResponseDataItemRelationshipsPledgeHistoryDataItem = { + id: string; + type: MembersResponseResponseDataItemRelationshipsPledgeHistoryDataItemType; +}; + +export type MembersResponseResponseDataItemRelationshipsPledgeHistory = { + data: MembersResponseResponseDataItemRelationshipsPledgeHistoryDataItem[]; +}; + +export type MembersResponseResponseDataItemRelationshipsUserDataType = + (typeof MembersResponseResponseDataItemRelationshipsUserDataType)[keyof typeof MembersResponseResponseDataItemRelationshipsUserDataType]; + +export const MembersResponseResponseDataItemRelationshipsUserDataType = { + user: "user" +} as const; + +export type MembersResponseResponseDataItemRelationshipsUserData = { + id: string; + type: MembersResponseResponseDataItemRelationshipsUserDataType; +}; + +export type MembersResponseResponseDataItemRelationshipsUserLinks = { + related?: string; +}; + +export type MembersResponseResponseDataItemRelationshipsUser = { + data: MembersResponseResponseDataItemRelationshipsUserData; + links?: MembersResponseResponseDataItemRelationshipsUserLinks; +}; + +export type MembersResponseResponseDataItemRelationships = { + address?: MembersResponseResponseDataItemRelationshipsAddress; + campaign?: MembersResponseResponseDataItemRelationshipsCampaign; + currently_entitled_tiers?: MembersResponseResponseDataItemRelationshipsCurrentlyEntitledTiers; + pledge_history?: MembersResponseResponseDataItemRelationshipsPledgeHistory; + user?: MembersResponseResponseDataItemRelationshipsUser; +}; + +export type MembersResponseResponseDataItem = { + id: string; + type: MembersResponseResponseDataItemType; + attributes?: Member; + relationships?: MembersResponseResponseDataItemRelationships; +}; + +export type MembersResponseResponseIncludedItem = + | { + id: string; + type: "address"; + attributes: Address; + } + | { + id: string; + type: "campaign"; + attributes: Campaign; + } + | { + id: string; + type: "tier"; + attributes: Tier; + } + | { + id: string; + type: "pledge-event"; + attributes: PledgeEvent; + } + | { + id: string; + type: "user"; + attributes: User; + }; + +export type MembersResponseResponse = { + data: MembersResponseResponseDataItem[]; + included?: MembersResponseResponseIncludedItem[]; + meta: JSONAPIResponseMeta; +}; + +export type MemberResponseResponseDataType = + (typeof MemberResponseResponseDataType)[keyof typeof MemberResponseResponseDataType]; + +export const MemberResponseResponseDataType = { + member: "member" +} as const; + +export type MemberResponseResponseDataRelationshipsAddressDataType = + (typeof MemberResponseResponseDataRelationshipsAddressDataType)[keyof typeof MemberResponseResponseDataRelationshipsAddressDataType]; + +export const MemberResponseResponseDataRelationshipsAddressDataType = { + address: "address" +} as const; + +export type MemberResponseResponseDataRelationshipsAddressData = { + id: string; + type: MemberResponseResponseDataRelationshipsAddressDataType; +}; + +export type MemberResponseResponseDataRelationshipsAddressLinks = { + related?: string; +}; + +export type MemberResponseResponseDataRelationshipsAddress = { + data: MemberResponseResponseDataRelationshipsAddressData; + links?: MemberResponseResponseDataRelationshipsAddressLinks; +}; + +export type MemberResponseResponseDataRelationshipsCampaignDataType = + (typeof MemberResponseResponseDataRelationshipsCampaignDataType)[keyof typeof MemberResponseResponseDataRelationshipsCampaignDataType]; + +export const MemberResponseResponseDataRelationshipsCampaignDataType = { + campaign: "campaign" +} as const; + +export type MemberResponseResponseDataRelationshipsCampaignData = { + id: string; + type: MemberResponseResponseDataRelationshipsCampaignDataType; +}; + +export type MemberResponseResponseDataRelationshipsCampaignLinks = { + related?: string; +}; + +export type MemberResponseResponseDataRelationshipsCampaign = { + data: MemberResponseResponseDataRelationshipsCampaignData; + links?: MemberResponseResponseDataRelationshipsCampaignLinks; +}; + +export type MemberResponseResponseDataRelationshipsCurrentlyEntitledTiersDataItemType = + (typeof MemberResponseResponseDataRelationshipsCurrentlyEntitledTiersDataItemType)[keyof typeof MemberResponseResponseDataRelationshipsCurrentlyEntitledTiersDataItemType]; + +export const MemberResponseResponseDataRelationshipsCurrentlyEntitledTiersDataItemType = { + tier: "tier" +} as const; + +export type MemberResponseResponseDataRelationshipsCurrentlyEntitledTiersDataItem = { + id: string; + type: MemberResponseResponseDataRelationshipsCurrentlyEntitledTiersDataItemType; +}; + +export type MemberResponseResponseDataRelationshipsCurrentlyEntitledTiers = { + data: MemberResponseResponseDataRelationshipsCurrentlyEntitledTiersDataItem[]; +}; + +export type MemberResponseResponseDataRelationshipsPledgeHistoryDataItemType = + (typeof MemberResponseResponseDataRelationshipsPledgeHistoryDataItemType)[keyof typeof MemberResponseResponseDataRelationshipsPledgeHistoryDataItemType]; + +export const MemberResponseResponseDataRelationshipsPledgeHistoryDataItemType = { + "pledge-event": "pledge-event" +} as const; + +export type MemberResponseResponseDataRelationshipsPledgeHistoryDataItem = { + id: string; + type: MemberResponseResponseDataRelationshipsPledgeHistoryDataItemType; +}; + +export type MemberResponseResponseDataRelationshipsPledgeHistory = { + data: MemberResponseResponseDataRelationshipsPledgeHistoryDataItem[]; +}; + +export type MemberResponseResponseDataRelationshipsUserDataType = + (typeof MemberResponseResponseDataRelationshipsUserDataType)[keyof typeof MemberResponseResponseDataRelationshipsUserDataType]; + +export const MemberResponseResponseDataRelationshipsUserDataType = { + user: "user" +} as const; + +export type MemberResponseResponseDataRelationshipsUserData = { + id: string; + type: MemberResponseResponseDataRelationshipsUserDataType; +}; + +export type MemberResponseResponseDataRelationshipsUserLinks = { + related?: string; +}; + +export type MemberResponseResponseDataRelationshipsUser = { + data: MemberResponseResponseDataRelationshipsUserData; + links?: MemberResponseResponseDataRelationshipsUserLinks; +}; + +export type MemberResponseResponseDataRelationships = { + address?: MemberResponseResponseDataRelationshipsAddress; + campaign?: MemberResponseResponseDataRelationshipsCampaign; + currently_entitled_tiers?: MemberResponseResponseDataRelationshipsCurrentlyEntitledTiers; + pledge_history?: MemberResponseResponseDataRelationshipsPledgeHistory; + user?: MemberResponseResponseDataRelationshipsUser; +}; + +export type MemberResponseResponseData = { + id: string; + type: MemberResponseResponseDataType; + attributes?: Member; + relationships?: MemberResponseResponseDataRelationships; +}; + +export type MemberResponseResponseIncludedItem = + | { + id: string; + type: "address"; + attributes: Address; + } + | { + id: string; + type: "campaign"; + attributes: Campaign; + } + | { + id: string; + type: "tier"; + attributes: Tier; + } + | { + id: string; + type: "pledge-event"; + attributes: PledgeEvent; + } + | { + id: string; + type: "user"; + attributes: User; + }; + +export type MemberResponseResponse = { + data: MemberResponseResponseData; + included?: MemberResponseResponseIncludedItem[]; + links: JSONAPIResponseLinks; +}; + +export type PostsResponseResponseDataItemType = + (typeof PostsResponseResponseDataItemType)[keyof typeof PostsResponseResponseDataItemType]; + +export const PostsResponseResponseDataItemType = { + post: "post" +} as const; + +export type PostsResponseResponseDataItemRelationshipsUserDataType = + (typeof PostsResponseResponseDataItemRelationshipsUserDataType)[keyof typeof PostsResponseResponseDataItemRelationshipsUserDataType]; + +export const PostsResponseResponseDataItemRelationshipsUserDataType = { + user: "user" +} as const; + +export type PostsResponseResponseDataItemRelationshipsUserData = { + id: string; + type: PostsResponseResponseDataItemRelationshipsUserDataType; +}; + +export type PostsResponseResponseDataItemRelationshipsUserLinks = { + related?: string; +}; + +export type PostsResponseResponseDataItemRelationshipsUser = { + data: PostsResponseResponseDataItemRelationshipsUserData; + links?: PostsResponseResponseDataItemRelationshipsUserLinks; +}; + +export type PostsResponseResponseDataItemRelationshipsCampaignDataType = + (typeof PostsResponseResponseDataItemRelationshipsCampaignDataType)[keyof typeof PostsResponseResponseDataItemRelationshipsCampaignDataType]; + +export const PostsResponseResponseDataItemRelationshipsCampaignDataType = { + campaign: "campaign" +} as const; + +export type PostsResponseResponseDataItemRelationshipsCampaignData = { + id: string; + type: PostsResponseResponseDataItemRelationshipsCampaignDataType; +}; + +export type PostsResponseResponseDataItemRelationshipsCampaignLinks = { + related?: string; +}; + +export type PostsResponseResponseDataItemRelationshipsCampaign = { + data: PostsResponseResponseDataItemRelationshipsCampaignData; + links?: PostsResponseResponseDataItemRelationshipsCampaignLinks; +}; + +export type PostsResponseResponseDataItemRelationships = { + user?: PostsResponseResponseDataItemRelationshipsUser; + campaign?: PostsResponseResponseDataItemRelationshipsCampaign; +}; + +export type PostsResponseResponseDataItem = { + id: string; + type: PostsResponseResponseDataItemType; + attributes?: Post; + relationships?: PostsResponseResponseDataItemRelationships; +}; + +export type PostsResponseResponseIncludedItem = + | { + id: string; + type: "user"; + attributes: User; + } + | { + id: string; + type: "campaign"; + attributes: Campaign; + }; + +export type PostsResponseResponse = { + data: PostsResponseResponseDataItem[]; + included?: PostsResponseResponseIncludedItem[]; + meta: JSONAPIResponseMeta; +}; + +export type PostResponseResponseDataType = + (typeof PostResponseResponseDataType)[keyof typeof PostResponseResponseDataType]; + +export const PostResponseResponseDataType = { + post: "post" +} as const; + +export type PostResponseResponseDataRelationshipsUserDataType = + (typeof PostResponseResponseDataRelationshipsUserDataType)[keyof typeof PostResponseResponseDataRelationshipsUserDataType]; + +export const PostResponseResponseDataRelationshipsUserDataType = { + user: "user" +} as const; + +export type PostResponseResponseDataRelationshipsUserData = { + id: string; + type: PostResponseResponseDataRelationshipsUserDataType; +}; + +export type PostResponseResponseDataRelationshipsUserLinks = { + related?: string; +}; + +export type PostResponseResponseDataRelationshipsUser = { + data: PostResponseResponseDataRelationshipsUserData; + links?: PostResponseResponseDataRelationshipsUserLinks; +}; + +export type PostResponseResponseDataRelationshipsCampaignDataType = + (typeof PostResponseResponseDataRelationshipsCampaignDataType)[keyof typeof PostResponseResponseDataRelationshipsCampaignDataType]; + +export const PostResponseResponseDataRelationshipsCampaignDataType = { + campaign: "campaign" +} as const; + +export type PostResponseResponseDataRelationshipsCampaignData = { + id: string; + type: PostResponseResponseDataRelationshipsCampaignDataType; +}; + +export type PostResponseResponseDataRelationshipsCampaignLinks = { + related?: string; +}; + +export type PostResponseResponseDataRelationshipsCampaign = { + data: PostResponseResponseDataRelationshipsCampaignData; + links?: PostResponseResponseDataRelationshipsCampaignLinks; +}; + +export type PostResponseResponseDataRelationships = { + user?: PostResponseResponseDataRelationshipsUser; + campaign?: PostResponseResponseDataRelationshipsCampaign; +}; + +export type PostResponseResponseData = { + id: string; + type: PostResponseResponseDataType; + attributes?: Post; + relationships?: PostResponseResponseDataRelationships; +}; + +export type PostResponseResponseIncludedItem = + | { + id: string; + type: "user"; + attributes: User; + } + | { + id: string; + type: "campaign"; + attributes: Campaign; + }; + +export type PostResponseResponse = { + data: PostResponseResponseData; + included?: PostResponseResponseIncludedItem[]; + links: JSONAPIResponseLinks; +}; + +export type LiveResponseResponseDataType = + (typeof LiveResponseResponseDataType)[keyof typeof LiveResponseResponseDataType]; + +export const LiveResponseResponseDataType = { + live: "live" +} as const; + +export type LiveResponseResponseDataRelationshipsCampaignDataType = + (typeof LiveResponseResponseDataRelationshipsCampaignDataType)[keyof typeof LiveResponseResponseDataRelationshipsCampaignDataType]; + +export const LiveResponseResponseDataRelationshipsCampaignDataType = { + campaign: "campaign" +} as const; + +export type LiveResponseResponseDataRelationshipsCampaignData = { + id: string; + type: LiveResponseResponseDataRelationshipsCampaignDataType; +}; + +export type LiveResponseResponseDataRelationshipsCampaignLinks = { + related?: string; +}; + +export type LiveResponseResponseDataRelationshipsCampaign = { + data: LiveResponseResponseDataRelationshipsCampaignData; + links?: LiveResponseResponseDataRelationshipsCampaignLinks; +}; + +export type LiveResponseResponseDataRelationshipsLiveAccessRulesDataItemType = + (typeof LiveResponseResponseDataRelationshipsLiveAccessRulesDataItemType)[keyof typeof LiveResponseResponseDataRelationshipsLiveAccessRulesDataItemType]; + +export const LiveResponseResponseDataRelationshipsLiveAccessRulesDataItemType = { + "live-access-rule": "live-access-rule" +} as const; + +export type LiveResponseResponseDataRelationshipsLiveAccessRulesDataItem = { + id: string; + type: LiveResponseResponseDataRelationshipsLiveAccessRulesDataItemType; +}; + +export type LiveResponseResponseDataRelationshipsLiveAccessRules = { + data: LiveResponseResponseDataRelationshipsLiveAccessRulesDataItem[]; +}; + +export type LiveResponseResponseDataRelationships = { + campaign?: LiveResponseResponseDataRelationshipsCampaign; + live_access_rules?: LiveResponseResponseDataRelationshipsLiveAccessRules; +}; + +export type LiveResponseResponseData = { + id: string; + type: LiveResponseResponseDataType; + attributes?: Live; + relationships?: LiveResponseResponseDataRelationships; +}; + +export type LiveResponseResponseIncludedItem = + | { + id: string; + type: "campaign"; + attributes: Campaign; + } + | { + id: string; + type: "live-access-rule"; + attributes: LiveAccessRule; + }; + +export type LiveResponseResponse = { + data: LiveResponseResponseData; + included?: LiveResponseResponseIncludedItem[]; + links: JSONAPIResponseLinks; +}; + +export type WebhookResponseResponseDataType = + (typeof WebhookResponseResponseDataType)[keyof typeof WebhookResponseResponseDataType]; + +export const WebhookResponseResponseDataType = { + webhook: "webhook" +} as const; + +export type WebhookResponseResponseDataRelationshipsCampaignDataType = + (typeof WebhookResponseResponseDataRelationshipsCampaignDataType)[keyof typeof WebhookResponseResponseDataRelationshipsCampaignDataType]; + +export const WebhookResponseResponseDataRelationshipsCampaignDataType = { + campaign: "campaign" +} as const; + +export type WebhookResponseResponseDataRelationshipsCampaignData = { + id: string; + type: WebhookResponseResponseDataRelationshipsCampaignDataType; +}; + +export type WebhookResponseResponseDataRelationshipsCampaignLinks = { + related?: string; +}; + +export type WebhookResponseResponseDataRelationshipsCampaign = { + data: WebhookResponseResponseDataRelationshipsCampaignData; + links?: WebhookResponseResponseDataRelationshipsCampaignLinks; +}; + +export type WebhookResponseResponseDataRelationshipsClientDataType = + (typeof WebhookResponseResponseDataRelationshipsClientDataType)[keyof typeof WebhookResponseResponseDataRelationshipsClientDataType]; + +export const WebhookResponseResponseDataRelationshipsClientDataType = { + client: "client" +} as const; + +export type WebhookResponseResponseDataRelationshipsClientData = { + id: string; + type: WebhookResponseResponseDataRelationshipsClientDataType; +}; + +export type WebhookResponseResponseDataRelationshipsClientLinks = { + related?: string; +}; + +export type WebhookResponseResponseDataRelationshipsClient = { + data: WebhookResponseResponseDataRelationshipsClientData; + links?: WebhookResponseResponseDataRelationshipsClientLinks; +}; + +export type WebhookResponseResponseDataRelationships = { + campaign?: WebhookResponseResponseDataRelationshipsCampaign; + client?: WebhookResponseResponseDataRelationshipsClient; +}; + +export type WebhookResponseResponseData = { + id: string; + type: WebhookResponseResponseDataType; + attributes?: Webhook; + relationships?: WebhookResponseResponseDataRelationships; +}; + +export type WebhookResponseResponseIncludedItem = + | { + id: string; + type: "campaign"; + attributes: Campaign; + } + | { + id: string; + type: "client"; + attributes: Client; + }; + +export type WebhookResponseResponse = { + data: WebhookResponseResponseData; + included?: WebhookResponseResponseIncludedItem[]; + links: JSONAPIResponseLinks; +}; + +export type WebhooksResponseResponseDataItemType = + (typeof WebhooksResponseResponseDataItemType)[keyof typeof WebhooksResponseResponseDataItemType]; + +export const WebhooksResponseResponseDataItemType = { + webhook: "webhook" +} as const; + +export type WebhooksResponseResponseDataItemRelationshipsCampaignDataType = + (typeof WebhooksResponseResponseDataItemRelationshipsCampaignDataType)[keyof typeof WebhooksResponseResponseDataItemRelationshipsCampaignDataType]; + +export const WebhooksResponseResponseDataItemRelationshipsCampaignDataType = { + campaign: "campaign" +} as const; + +export type WebhooksResponseResponseDataItemRelationshipsCampaignData = { + id: string; + type: WebhooksResponseResponseDataItemRelationshipsCampaignDataType; +}; + +export type WebhooksResponseResponseDataItemRelationshipsCampaignLinks = { + related?: string; +}; + +export type WebhooksResponseResponseDataItemRelationshipsCampaign = { + data: WebhooksResponseResponseDataItemRelationshipsCampaignData; + links?: WebhooksResponseResponseDataItemRelationshipsCampaignLinks; +}; + +export type WebhooksResponseResponseDataItemRelationshipsClientDataType = + (typeof WebhooksResponseResponseDataItemRelationshipsClientDataType)[keyof typeof WebhooksResponseResponseDataItemRelationshipsClientDataType]; + +export const WebhooksResponseResponseDataItemRelationshipsClientDataType = { + client: "client" +} as const; + +export type WebhooksResponseResponseDataItemRelationshipsClientData = { + id: string; + type: WebhooksResponseResponseDataItemRelationshipsClientDataType; +}; + +export type WebhooksResponseResponseDataItemRelationshipsClientLinks = { + related?: string; +}; + +export type WebhooksResponseResponseDataItemRelationshipsClient = { + data: WebhooksResponseResponseDataItemRelationshipsClientData; + links?: WebhooksResponseResponseDataItemRelationshipsClientLinks; +}; + +export type WebhooksResponseResponseDataItemRelationships = { + campaign?: WebhooksResponseResponseDataItemRelationshipsCampaign; + client?: WebhooksResponseResponseDataItemRelationshipsClient; +}; + +export type WebhooksResponseResponseDataItem = { + id: string; + type: WebhooksResponseResponseDataItemType; + attributes?: Webhook; + relationships?: WebhooksResponseResponseDataItemRelationships; +}; + +export type WebhooksResponseResponseIncludedItem = + | { + id: string; + type: "campaign"; + attributes: Campaign; + } + | { + id: string; + type: "client"; + attributes: Client; + }; + +export type WebhooksResponseResponse = { + data: WebhooksResponseResponseDataItem[]; + included?: WebhooksResponseResponseIncludedItem[]; + meta: JSONAPIResponseMeta; +}; + +export type UserAgentParameter = string; + +export type GetIdentityParams = { + include?: GetIdentityIncludeItem[]; + fields?: { + campaign?: Campaign[]; + member?: Member[]; + user?: User[]; + }; +}; + +export type GetIdentityIncludeItem = (typeof GetIdentityIncludeItem)[keyof typeof GetIdentityIncludeItem]; + +export const GetIdentityIncludeItem = { + campaign: "campaign", + memberships: "memberships" +} as const; + +export type GetCampaignParams = { + include?: GetCampaignIncludeItem[]; + fields?: { + benefit?: Benefit[]; + user?: User[]; + goal?: Goal[]; + "live-access-rule"?: LiveAccessRule[]; + tier?: Tier[]; + campaign?: Campaign[]; + }; +}; + +export type GetCampaignIncludeItem = (typeof GetCampaignIncludeItem)[keyof typeof GetCampaignIncludeItem]; + +export const GetCampaignIncludeItem = { + benefits: "benefits", + creator: "creator", + goals: "goals", + live_access_rules: "live_access_rules", + tiers: "tiers" +} as const; + +export type GetCampaignsParams = { + include?: GetCampaignsIncludeItem[]; + fields?: { + benefit?: Benefit[]; + user?: User[]; + goal?: Goal[]; + "live-access-rule"?: LiveAccessRule[]; + tier?: Tier[]; + campaign?: Campaign[]; + }; + sort?: string; + page?: { + cursor?: string; + count?: number; + }; +}; + +export type GetCampaignsIncludeItem = (typeof GetCampaignsIncludeItem)[keyof typeof GetCampaignsIncludeItem]; + +export const GetCampaignsIncludeItem = { + benefits: "benefits", + creator: "creator", + goals: "goals", + live_access_rules: "live_access_rules", + tiers: "tiers" +} as const; + +export type GetCampaignMembersParams = { + include?: GetCampaignMembersIncludeItem[]; + fields?: { + address?: Address[]; + campaign?: Campaign[]; + tier?: Tier[]; + "pledge-event"?: PledgeEvent[]; + user?: User[]; + member?: Member[]; + }; + sort?: string; + page?: { + cursor?: string; + count?: number; + }; +}; + +export type GetCampaignMembersIncludeItem = + (typeof GetCampaignMembersIncludeItem)[keyof typeof GetCampaignMembersIncludeItem]; + +export const GetCampaignMembersIncludeItem = { + address: "address", + campaign: "campaign", + currently_entitled_tiers: "currently_entitled_tiers", + pledge_history: "pledge_history", + user: "user" +} as const; + +export type GetMemberParams = { + include?: GetMemberIncludeItem[]; + fields?: { + address?: Address[]; + campaign?: Campaign[]; + tier?: Tier[]; + "pledge-event"?: PledgeEvent[]; + user?: User[]; + member?: Member[]; + }; +}; + +export type GetMemberIncludeItem = (typeof GetMemberIncludeItem)[keyof typeof GetMemberIncludeItem]; + +export const GetMemberIncludeItem = { + address: "address", + campaign: "campaign", + currently_entitled_tiers: "currently_entitled_tiers", + pledge_history: "pledge_history", + user: "user" +} as const; + +export type GetCampaignPostsParams = { + include?: GetCampaignPostsIncludeItem[]; + fields?: { + user?: User[]; + campaign?: Campaign[]; + post?: Post[]; + }; + sort?: string; + page?: { + cursor?: string; + count?: number; + }; +}; + +export type GetCampaignPostsIncludeItem = + (typeof GetCampaignPostsIncludeItem)[keyof typeof GetCampaignPostsIncludeItem]; + +export const GetCampaignPostsIncludeItem = { + user: "user", + campaign: "campaign" +} as const; + +export type GetPostParams = { + include?: GetPostIncludeItem[]; + fields?: { + user?: User[]; + campaign?: Campaign[]; + post?: Post[]; + }; +}; + +export type GetPostIncludeItem = (typeof GetPostIncludeItem)[keyof typeof GetPostIncludeItem]; + +export const GetPostIncludeItem = { + user: "user", + campaign: "campaign" +} as const; + +export type GetLiveParams = { + include?: GetLiveIncludeItem[]; + fields?: { + campaign?: Campaign[]; + "live-access-rule"?: LiveAccessRule[]; + live?: Live[]; + }; +}; + +export type GetLiveIncludeItem = (typeof GetLiveIncludeItem)[keyof typeof GetLiveIncludeItem]; + +export const GetLiveIncludeItem = { + campaign: "campaign", + live_access_rules: "live_access_rules" +} as const; + +export type EditLiveBodyDataType = (typeof EditLiveBodyDataType)[keyof typeof EditLiveBodyDataType]; + +export const EditLiveBodyDataType = { + live: "live" +} as const; + +export type EditLiveBodyDataAttributes = { + state: string; +}; + +export type EditLiveBodyData = { + type: EditLiveBodyDataType; + attributes: EditLiveBodyDataAttributes; + id: string; +}; + +export type EditLiveBody = { + data: EditLiveBodyData; +}; + +export type CreateLiveBodyDataType = (typeof CreateLiveBodyDataType)[keyof typeof CreateLiveBodyDataType]; + +export const CreateLiveBodyDataType = { + live: "live" +} as const; + +export type CreateLiveBodyDataAttributes = { + state: string; + /** @nullable */ + title?: string | null; + /** @nullable */ + description?: string | null; + /** @nullable */ + scheduled_for?: string | null; + live_access_rule_ids?: unknown[]; +}; + +export type CreateLiveBodyData = { + type: CreateLiveBodyDataType; + attributes: CreateLiveBodyDataAttributes; +}; + +export type CreateLiveBody = { + data: CreateLiveBodyData; +}; + +export type EditWebhookBodyDataType = (typeof EditWebhookBodyDataType)[keyof typeof EditWebhookBodyDataType]; + +export const EditWebhookBodyDataType = { + webhook: "webhook" +} as const; + +export type EditWebhookBodyDataAttributes = { + triggers?: WebhookTrigger; + /** Fully qualified uri where webhook will be sent */ + uri?: string; + paused?: boolean; +}; + +export type EditWebhookBodyData = { + type: EditWebhookBodyDataType; + id: string; + attributes: EditWebhookBodyDataAttributes; +}; + +export type EditWebhookBody = { + data: EditWebhookBodyData; +}; + +export type GetWebhooksParams = { + include?: GetWebhooksIncludeItem[]; + fields?: { + campaign?: Campaign[]; + client?: Client[]; + webhook?: Webhook[]; + }; + sort?: string; + page?: { + cursor?: string; + count?: number; + }; +}; + +export type GetWebhooksIncludeItem = (typeof GetWebhooksIncludeItem)[keyof typeof GetWebhooksIncludeItem]; + +export const GetWebhooksIncludeItem = { + campaign: "campaign", + client: "client" +} as const; + +export type CreateWebhookBodyDataType = (typeof CreateWebhookBodyDataType)[keyof typeof CreateWebhookBodyDataType]; + +export const CreateWebhookBodyDataType = { + webhook: "webhook" +} as const; + +export type CreateWebhookBodyDataAttributes = { + triggers: WebhookTrigger; + /** Fully qualified uri where webhook will be sent */ + uri: string; +}; + +export type CreateWebhookBodyDataRelationshipsCampaignDataType = + (typeof CreateWebhookBodyDataRelationshipsCampaignDataType)[keyof typeof CreateWebhookBodyDataRelationshipsCampaignDataType]; + +export const CreateWebhookBodyDataRelationshipsCampaignDataType = { + campaign: "campaign" +} as const; + +export type CreateWebhookBodyDataRelationshipsCampaignData = { + type: CreateWebhookBodyDataRelationshipsCampaignDataType; + id: string; +}; + +export type CreateWebhookBodyDataRelationshipsCampaign = { + data?: CreateWebhookBodyDataRelationshipsCampaignData; +}; + +export type CreateWebhookBodyDataRelationships = { + campaign?: CreateWebhookBodyDataRelationshipsCampaign; +}; + +export type CreateWebhookBodyData = { + type: CreateWebhookBodyDataType; + attributes: CreateWebhookBodyDataAttributes; + relationships: CreateWebhookBodyDataRelationships; +}; + +export type CreateWebhookBody = { + data: CreateWebhookBodyData; +}; + +export type getIdentityResponse200 = { + data: UserResponseResponse; + status: 200; +}; + +export type getIdentityResponse400 = { + data: N400Response; + status: 400; +}; + +export type getIdentityResponse401 = { + data: N401Response; + status: 401; +}; + +export type getIdentityResponse403 = { + data: N403Response; + status: 403; +}; + +export type getIdentityResponse404 = { + data: N404Response; + status: 404; +}; + +export type getIdentityResponse405 = { + data: N405Response; + status: 405; +}; + +export type getIdentityResponse406 = { + data: N406Response; + status: 406; +}; + +export type getIdentityResponse410 = { + data: N410Response; + status: 410; +}; + +export type getIdentityResponse429 = { + data: N429Response; + status: 429; +}; + +export type getIdentityResponse500 = { + data: N500Response; + status: 500; +}; + +export type getIdentityResponse503 = { + data: N503Response; + status: 503; +}; + +export type getIdentityResponseSuccess = getIdentityResponse200 & { + headers: Headers; +}; +export type getIdentityResponseError = ( + | getIdentityResponse400 + | getIdentityResponse401 + | getIdentityResponse403 + | getIdentityResponse404 + | getIdentityResponse405 + | getIdentityResponse406 + | getIdentityResponse410 + | getIdentityResponse429 + | getIdentityResponse500 + | getIdentityResponse503 +) & { + headers: Headers; +}; + +export type getIdentityResponse = getIdentityResponseSuccess | getIdentityResponseError; + +export const getGetIdentityUrl = (params?: GetIdentityParams) => { + const normalizedParams = new URLSearchParams(); + const deepObjectEntries: string[] = []; + + Object.entries(params || {}).forEach(([key, value]) => { + const deepObjectParameters = ["fields"]; + + if (typeof value === "object" && value !== null && !Array.isArray(value) && deepObjectParameters.includes(key)) { + Object.entries(value).forEach(([subKey, subValue]) => { + if (subValue !== undefined) { + deepObjectEntries.push( + encodeURIComponent(key) + + "[" + + encodeURIComponent(subKey) + + "]=" + + (subValue === null ? "null" : encodeURIComponent(String(subValue))) + ); + } + }); + return; + } + + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : String(value)); + } + }); + + const stringifiedParams = [normalizedParams.toString(), deepObjectEntries.join("&")].filter(Boolean).join("&"); + + return stringifiedParams.length > 0 ? `/api/oauth2/v2/identity?${stringifiedParams}` : `/api/oauth2/v2/identity`; +}; + +/** @summary Get identity */ +export const getIdentity = async (params?: GetIdentityParams, options?: RequestInit): Promise => { + const res = await fetch(getGetIdentityUrl(params), { + ...options, + method: "GET" + }); + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getIdentityResponse["data"] = body ? JSON.parse(body) : {}; + return { data, status: res.status, headers: res.headers } as getIdentityResponse; +}; + +export type getCampaignResponse200 = { + data: CampaignResponseResponse; + status: 200; +}; + +export type getCampaignResponse400 = { + data: N400Response; + status: 400; +}; + +export type getCampaignResponse401 = { + data: N401Response; + status: 401; +}; + +export type getCampaignResponse403 = { + data: N403Response; + status: 403; +}; + +export type getCampaignResponse404 = { + data: N404Response; + status: 404; +}; + +export type getCampaignResponse405 = { + data: N405Response; + status: 405; +}; + +export type getCampaignResponse406 = { + data: N406Response; + status: 406; +}; + +export type getCampaignResponse410 = { + data: N410Response; + status: 410; +}; + +export type getCampaignResponse429 = { + data: N429Response; + status: 429; +}; + +export type getCampaignResponse500 = { + data: N500Response; + status: 500; +}; + +export type getCampaignResponse503 = { + data: N503Response; + status: 503; +}; + +export type getCampaignResponseSuccess = getCampaignResponse200 & { + headers: Headers; +}; +export type getCampaignResponseError = ( + | getCampaignResponse400 + | getCampaignResponse401 + | getCampaignResponse403 + | getCampaignResponse404 + | getCampaignResponse405 + | getCampaignResponse406 + | getCampaignResponse410 + | getCampaignResponse429 + | getCampaignResponse500 + | getCampaignResponse503 +) & { + headers: Headers; +}; + +export type getCampaignResponse = getCampaignResponseSuccess | getCampaignResponseError; + +export const getGetCampaignUrl = (campaignId: string, params?: GetCampaignParams) => { + const normalizedParams = new URLSearchParams(); + const deepObjectEntries: string[] = []; + + Object.entries(params || {}).forEach(([key, value]) => { + const deepObjectParameters = ["fields"]; + + if (typeof value === "object" && value !== null && !Array.isArray(value) && deepObjectParameters.includes(key)) { + Object.entries(value).forEach(([subKey, subValue]) => { + if (subValue !== undefined) { + deepObjectEntries.push( + encodeURIComponent(key) + + "[" + + encodeURIComponent(subKey) + + "]=" + + (subValue === null ? "null" : encodeURIComponent(String(subValue))) + ); + } + }); + return; + } + + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : String(value)); + } + }); + + const stringifiedParams = [normalizedParams.toString(), deepObjectEntries.join("&")].filter(Boolean).join("&"); + + return stringifiedParams.length > 0 + ? `/api/oauth2/v2/campaigns/${campaignId}?${stringifiedParams}` + : `/api/oauth2/v2/campaigns/${campaignId}`; +}; + +/** @summary Get campaign */ +export const getCampaign = async ( + campaignId: string, + params?: GetCampaignParams, + options?: RequestInit +): Promise => { + const res = await fetch(getGetCampaignUrl(campaignId, params), { + ...options, + method: "GET" + }); + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getCampaignResponse["data"] = body ? JSON.parse(body) : {}; + return { data, status: res.status, headers: res.headers } as getCampaignResponse; +}; + +export type getCampaignsResponse200 = { + data: CampaignsResponseResponse; + status: 200; +}; + +export type getCampaignsResponse400 = { + data: N400Response; + status: 400; +}; + +export type getCampaignsResponse401 = { + data: N401Response; + status: 401; +}; + +export type getCampaignsResponse403 = { + data: N403Response; + status: 403; +}; + +export type getCampaignsResponse404 = { + data: N404Response; + status: 404; +}; + +export type getCampaignsResponse405 = { + data: N405Response; + status: 405; +}; + +export type getCampaignsResponse406 = { + data: N406Response; + status: 406; +}; + +export type getCampaignsResponse410 = { + data: N410Response; + status: 410; +}; + +export type getCampaignsResponse429 = { + data: N429Response; + status: 429; +}; + +export type getCampaignsResponse500 = { + data: N500Response; + status: 500; +}; + +export type getCampaignsResponse503 = { + data: N503Response; + status: 503; +}; + +export type getCampaignsResponseSuccess = getCampaignsResponse200 & { + headers: Headers; +}; +export type getCampaignsResponseError = ( + | getCampaignsResponse400 + | getCampaignsResponse401 + | getCampaignsResponse403 + | getCampaignsResponse404 + | getCampaignsResponse405 + | getCampaignsResponse406 + | getCampaignsResponse410 + | getCampaignsResponse429 + | getCampaignsResponse500 + | getCampaignsResponse503 +) & { + headers: Headers; +}; + +export type getCampaignsResponse = getCampaignsResponseSuccess | getCampaignsResponseError; + +export const getGetCampaignsUrl = (params?: GetCampaignsParams) => { + const normalizedParams = new URLSearchParams(); + const deepObjectEntries: string[] = []; + + Object.entries(params || {}).forEach(([key, value]) => { + const deepObjectParameters = ["fields", "page"]; + + if (typeof value === "object" && value !== null && !Array.isArray(value) && deepObjectParameters.includes(key)) { + Object.entries(value).forEach(([subKey, subValue]) => { + if (subValue !== undefined) { + deepObjectEntries.push( + encodeURIComponent(key) + + "[" + + encodeURIComponent(subKey) + + "]=" + + (subValue === null ? "null" : encodeURIComponent(String(subValue))) + ); + } + }); + return; + } + + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : String(value)); + } + }); + + const stringifiedParams = [normalizedParams.toString(), deepObjectEntries.join("&")].filter(Boolean).join("&"); + + return stringifiedParams.length > 0 ? `/api/oauth2/v2/campaigns?${stringifiedParams}` : `/api/oauth2/v2/campaigns`; +}; + +/** @summary Get campaigns */ +export const getCampaigns = async ( + params?: GetCampaignsParams, + options?: RequestInit +): Promise => { + const res = await fetch(getGetCampaignsUrl(params), { + ...options, + method: "GET" + }); + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getCampaignsResponse["data"] = body ? JSON.parse(body) : {}; + return { data, status: res.status, headers: res.headers } as getCampaignsResponse; +}; + +export type getCampaignMembersResponse200 = { + data: MembersResponseResponse; + status: 200; +}; + +export type getCampaignMembersResponse400 = { + data: N400Response; + status: 400; +}; + +export type getCampaignMembersResponse401 = { + data: N401Response; + status: 401; +}; + +export type getCampaignMembersResponse403 = { + data: N403Response; + status: 403; +}; + +export type getCampaignMembersResponse404 = { + data: N404Response; + status: 404; +}; + +export type getCampaignMembersResponse405 = { + data: N405Response; + status: 405; +}; + +export type getCampaignMembersResponse406 = { + data: N406Response; + status: 406; +}; + +export type getCampaignMembersResponse410 = { + data: N410Response; + status: 410; +}; + +export type getCampaignMembersResponse429 = { + data: N429Response; + status: 429; +}; + +export type getCampaignMembersResponse500 = { + data: N500Response; + status: 500; +}; + +export type getCampaignMembersResponse503 = { + data: N503Response; + status: 503; +}; + +export type getCampaignMembersResponseSuccess = getCampaignMembersResponse200 & { + headers: Headers; +}; +export type getCampaignMembersResponseError = ( + | getCampaignMembersResponse400 + | getCampaignMembersResponse401 + | getCampaignMembersResponse403 + | getCampaignMembersResponse404 + | getCampaignMembersResponse405 + | getCampaignMembersResponse406 + | getCampaignMembersResponse410 + | getCampaignMembersResponse429 + | getCampaignMembersResponse500 + | getCampaignMembersResponse503 +) & { + headers: Headers; +}; + +export type getCampaignMembersResponse = getCampaignMembersResponseSuccess | getCampaignMembersResponseError; + +export const getGetCampaignMembersUrl = (campaignId: string, params?: GetCampaignMembersParams) => { + const normalizedParams = new URLSearchParams(); + const deepObjectEntries: string[] = []; + + Object.entries(params || {}).forEach(([key, value]) => { + const deepObjectParameters = ["fields", "page"]; + + if (typeof value === "object" && value !== null && !Array.isArray(value) && deepObjectParameters.includes(key)) { + Object.entries(value).forEach(([subKey, subValue]) => { + if (subValue !== undefined) { + deepObjectEntries.push( + encodeURIComponent(key) + + "[" + + encodeURIComponent(subKey) + + "]=" + + (subValue === null ? "null" : encodeURIComponent(String(subValue))) + ); + } + }); + return; + } + + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : String(value)); + } + }); + + const stringifiedParams = [normalizedParams.toString(), deepObjectEntries.join("&")].filter(Boolean).join("&"); + + return stringifiedParams.length > 0 + ? `/api/oauth2/v2/campaigns/${campaignId}/members?${stringifiedParams}` + : `/api/oauth2/v2/campaigns/${campaignId}/members`; +}; + +/** @summary Get campaign members */ +export const getCampaignMembers = async ( + campaignId: string, + params?: GetCampaignMembersParams, + options?: RequestInit +): Promise => { + const res = await fetch(getGetCampaignMembersUrl(campaignId, params), { + ...options, + method: "GET" + }); + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getCampaignMembersResponse["data"] = body ? JSON.parse(body) : {}; + return { data, status: res.status, headers: res.headers } as getCampaignMembersResponse; +}; + +export type getMemberResponse200 = { + data: MemberResponseResponse; + status: 200; +}; + +export type getMemberResponse400 = { + data: N400Response; + status: 400; +}; + +export type getMemberResponse401 = { + data: N401Response; + status: 401; +}; + +export type getMemberResponse403 = { + data: N403Response; + status: 403; +}; + +export type getMemberResponse404 = { + data: N404Response; + status: 404; +}; + +export type getMemberResponse405 = { + data: N405Response; + status: 405; +}; + +export type getMemberResponse406 = { + data: N406Response; + status: 406; +}; + +export type getMemberResponse410 = { + data: N410Response; + status: 410; +}; + +export type getMemberResponse429 = { + data: N429Response; + status: 429; +}; + +export type getMemberResponse500 = { + data: N500Response; + status: 500; +}; + +export type getMemberResponse503 = { + data: N503Response; + status: 503; +}; + +export type getMemberResponseSuccess = getMemberResponse200 & { + headers: Headers; +}; +export type getMemberResponseError = ( + | getMemberResponse400 + | getMemberResponse401 + | getMemberResponse403 + | getMemberResponse404 + | getMemberResponse405 + | getMemberResponse406 + | getMemberResponse410 + | getMemberResponse429 + | getMemberResponse500 + | getMemberResponse503 +) & { + headers: Headers; +}; + +export type getMemberResponse = getMemberResponseSuccess | getMemberResponseError; + +export const getGetMemberUrl = (id: string, params?: GetMemberParams) => { + const normalizedParams = new URLSearchParams(); + const deepObjectEntries: string[] = []; + + Object.entries(params || {}).forEach(([key, value]) => { + const deepObjectParameters = ["fields"]; + + if (typeof value === "object" && value !== null && !Array.isArray(value) && deepObjectParameters.includes(key)) { + Object.entries(value).forEach(([subKey, subValue]) => { + if (subValue !== undefined) { + deepObjectEntries.push( + encodeURIComponent(key) + + "[" + + encodeURIComponent(subKey) + + "]=" + + (subValue === null ? "null" : encodeURIComponent(String(subValue))) + ); + } + }); + return; + } + + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : String(value)); + } + }); + + const stringifiedParams = [normalizedParams.toString(), deepObjectEntries.join("&")].filter(Boolean).join("&"); + + return stringifiedParams.length > 0 + ? `/api/oauth2/v2/members/${id}?${stringifiedParams}` + : `/api/oauth2/v2/members/${id}`; +}; + +/** @summary Get member */ +export const getMember = async ( + id: string, + params?: GetMemberParams, + options?: RequestInit +): Promise => { + const res = await fetch(getGetMemberUrl(id, params), { + ...options, + method: "GET" + }); + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getMemberResponse["data"] = body ? JSON.parse(body) : {}; + return { data, status: res.status, headers: res.headers } as getMemberResponse; +}; + +export type getCampaignPostsResponse200 = { + data: PostsResponseResponse; + status: 200; +}; + +export type getCampaignPostsResponse400 = { + data: N400Response; + status: 400; +}; + +export type getCampaignPostsResponse401 = { + data: N401Response; + status: 401; +}; + +export type getCampaignPostsResponse403 = { + data: N403Response; + status: 403; +}; + +export type getCampaignPostsResponse404 = { + data: N404Response; + status: 404; +}; + +export type getCampaignPostsResponse405 = { + data: N405Response; + status: 405; +}; + +export type getCampaignPostsResponse406 = { + data: N406Response; + status: 406; +}; + +export type getCampaignPostsResponse410 = { + data: N410Response; + status: 410; +}; + +export type getCampaignPostsResponse429 = { + data: N429Response; + status: 429; +}; + +export type getCampaignPostsResponse500 = { + data: N500Response; + status: 500; +}; + +export type getCampaignPostsResponse503 = { + data: N503Response; + status: 503; +}; + +export type getCampaignPostsResponseSuccess = getCampaignPostsResponse200 & { + headers: Headers; +}; +export type getCampaignPostsResponseError = ( + | getCampaignPostsResponse400 + | getCampaignPostsResponse401 + | getCampaignPostsResponse403 + | getCampaignPostsResponse404 + | getCampaignPostsResponse405 + | getCampaignPostsResponse406 + | getCampaignPostsResponse410 + | getCampaignPostsResponse429 + | getCampaignPostsResponse500 + | getCampaignPostsResponse503 +) & { + headers: Headers; +}; + +export type getCampaignPostsResponse = getCampaignPostsResponseSuccess | getCampaignPostsResponseError; + +export const getGetCampaignPostsUrl = (campaignId: string, params?: GetCampaignPostsParams) => { + const normalizedParams = new URLSearchParams(); + const deepObjectEntries: string[] = []; + + Object.entries(params || {}).forEach(([key, value]) => { + const deepObjectParameters = ["fields", "page"]; + + if (typeof value === "object" && value !== null && !Array.isArray(value) && deepObjectParameters.includes(key)) { + Object.entries(value).forEach(([subKey, subValue]) => { + if (subValue !== undefined) { + deepObjectEntries.push( + encodeURIComponent(key) + + "[" + + encodeURIComponent(subKey) + + "]=" + + (subValue === null ? "null" : encodeURIComponent(String(subValue))) + ); + } + }); + return; + } + + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : String(value)); + } + }); + + const stringifiedParams = [normalizedParams.toString(), deepObjectEntries.join("&")].filter(Boolean).join("&"); + + return stringifiedParams.length > 0 + ? `/api/oauth2/v2/campaigns/${campaignId}/posts?${stringifiedParams}` + : `/api/oauth2/v2/campaigns/${campaignId}/posts`; +}; + +/** @summary Get campaign posts */ +export const getCampaignPosts = async ( + campaignId: string, + params?: GetCampaignPostsParams, + options?: RequestInit +): Promise => { + const res = await fetch(getGetCampaignPostsUrl(campaignId, params), { + ...options, + method: "GET" + }); + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getCampaignPostsResponse["data"] = body ? JSON.parse(body) : {}; + return { data, status: res.status, headers: res.headers } as getCampaignPostsResponse; +}; + +export type getPostResponse200 = { + data: PostResponseResponse; + status: 200; +}; + +export type getPostResponse400 = { + data: N400Response; + status: 400; +}; + +export type getPostResponse401 = { + data: N401Response; + status: 401; +}; + +export type getPostResponse403 = { + data: N403Response; + status: 403; +}; + +export type getPostResponse404 = { + data: N404Response; + status: 404; +}; + +export type getPostResponse405 = { + data: N405Response; + status: 405; +}; + +export type getPostResponse406 = { + data: N406Response; + status: 406; +}; + +export type getPostResponse410 = { + data: N410Response; + status: 410; +}; + +export type getPostResponse429 = { + data: N429Response; + status: 429; +}; + +export type getPostResponse500 = { + data: N500Response; + status: 500; +}; + +export type getPostResponse503 = { + data: N503Response; + status: 503; +}; + +export type getPostResponseSuccess = getPostResponse200 & { + headers: Headers; +}; +export type getPostResponseError = ( + | getPostResponse400 + | getPostResponse401 + | getPostResponse403 + | getPostResponse404 + | getPostResponse405 + | getPostResponse406 + | getPostResponse410 + | getPostResponse429 + | getPostResponse500 + | getPostResponse503 +) & { + headers: Headers; +}; + +export type getPostResponse = getPostResponseSuccess | getPostResponseError; + +export const getGetPostUrl = (id: string, params?: GetPostParams) => { + const normalizedParams = new URLSearchParams(); + const deepObjectEntries: string[] = []; + + Object.entries(params || {}).forEach(([key, value]) => { + const deepObjectParameters = ["fields"]; + + if (typeof value === "object" && value !== null && !Array.isArray(value) && deepObjectParameters.includes(key)) { + Object.entries(value).forEach(([subKey, subValue]) => { + if (subValue !== undefined) { + deepObjectEntries.push( + encodeURIComponent(key) + + "[" + + encodeURIComponent(subKey) + + "]=" + + (subValue === null ? "null" : encodeURIComponent(String(subValue))) + ); + } + }); + return; + } + + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : String(value)); + } + }); + + const stringifiedParams = [normalizedParams.toString(), deepObjectEntries.join("&")].filter(Boolean).join("&"); + + return stringifiedParams.length > 0 + ? `/api/oauth2/v2/posts/${id}?${stringifiedParams}` + : `/api/oauth2/v2/posts/${id}`; +}; + +/** @summary Get post */ +export const getPost = async (id: string, params?: GetPostParams, options?: RequestInit): Promise => { + const res = await fetch(getGetPostUrl(id, params), { + ...options, + method: "GET" + }); + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getPostResponse["data"] = body ? JSON.parse(body) : {}; + return { data, status: res.status, headers: res.headers } as getPostResponse; +}; + +export type getLiveResponse200 = { + data: LiveResponseResponse; + status: 200; +}; + +export type getLiveResponse400 = { + data: N400Response; + status: 400; +}; + +export type getLiveResponse401 = { + data: N401Response; + status: 401; +}; + +export type getLiveResponse403 = { + data: N403Response; + status: 403; +}; + +export type getLiveResponse404 = { + data: N404Response; + status: 404; +}; + +export type getLiveResponse405 = { + data: N405Response; + status: 405; +}; + +export type getLiveResponse406 = { + data: N406Response; + status: 406; +}; + +export type getLiveResponse410 = { + data: N410Response; + status: 410; +}; + +export type getLiveResponse429 = { + data: N429Response; + status: 429; +}; + +export type getLiveResponse500 = { + data: N500Response; + status: 500; +}; + +export type getLiveResponse503 = { + data: N503Response; + status: 503; +}; + +export type getLiveResponseSuccess = getLiveResponse200 & { + headers: Headers; +}; +export type getLiveResponseError = ( + | getLiveResponse400 + | getLiveResponse401 + | getLiveResponse403 + | getLiveResponse404 + | getLiveResponse405 + | getLiveResponse406 + | getLiveResponse410 + | getLiveResponse429 + | getLiveResponse500 + | getLiveResponse503 +) & { + headers: Headers; +}; + +export type getLiveResponse = getLiveResponseSuccess | getLiveResponseError; + +export const getGetLiveUrl = (id: string, params?: GetLiveParams) => { + const normalizedParams = new URLSearchParams(); + const deepObjectEntries: string[] = []; + + Object.entries(params || {}).forEach(([key, value]) => { + const deepObjectParameters = ["fields"]; + + if (typeof value === "object" && value !== null && !Array.isArray(value) && deepObjectParameters.includes(key)) { + Object.entries(value).forEach(([subKey, subValue]) => { + if (subValue !== undefined) { + deepObjectEntries.push( + encodeURIComponent(key) + + "[" + + encodeURIComponent(subKey) + + "]=" + + (subValue === null ? "null" : encodeURIComponent(String(subValue))) + ); + } + }); + return; + } + + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : String(value)); + } + }); + + const stringifiedParams = [normalizedParams.toString(), deepObjectEntries.join("&")].filter(Boolean).join("&"); + + return stringifiedParams.length > 0 + ? `/api/oauth2/v2/lives/${id}?${stringifiedParams}` + : `/api/oauth2/v2/lives/${id}`; +}; + +/** @summary Get live */ +export const getLive = async (id: string, params?: GetLiveParams, options?: RequestInit): Promise => { + const res = await fetch(getGetLiveUrl(id, params), { + ...options, + method: "GET" + }); + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getLiveResponse["data"] = body ? JSON.parse(body) : {}; + return { data, status: res.status, headers: res.headers } as getLiveResponse; +}; + +export type deleteLiveResponse204 = { + data: N204Response; + status: 204; +}; + +export type deleteLiveResponse400 = { + data: N400Response; + status: 400; +}; + +export type deleteLiveResponse401 = { + data: N401Response; + status: 401; +}; + +export type deleteLiveResponse403 = { + data: N403Response; + status: 403; +}; + +export type deleteLiveResponse404 = { + data: N404Response; + status: 404; +}; + +export type deleteLiveResponse405 = { + data: N405Response; + status: 405; +}; + +export type deleteLiveResponse406 = { + data: N406Response; + status: 406; +}; + +export type deleteLiveResponse410 = { + data: N410Response; + status: 410; +}; + +export type deleteLiveResponse429 = { + data: N429Response; + status: 429; +}; + +export type deleteLiveResponse500 = { + data: N500Response; + status: 500; +}; + +export type deleteLiveResponse503 = { + data: N503Response; + status: 503; +}; + +export type deleteLiveResponseSuccess = deleteLiveResponse204 & { + headers: Headers; +}; +export type deleteLiveResponseError = ( + | deleteLiveResponse400 + | deleteLiveResponse401 + | deleteLiveResponse403 + | deleteLiveResponse404 + | deleteLiveResponse405 + | deleteLiveResponse406 + | deleteLiveResponse410 + | deleteLiveResponse429 + | deleteLiveResponse500 + | deleteLiveResponse503 +) & { + headers: Headers; +}; + +export type deleteLiveResponse = deleteLiveResponseSuccess | deleteLiveResponseError; + +export const getDeleteLiveUrl = (id: string) => { + return `/api/oauth2/v2/lives/${id}`; +}; + +/** @summary Delete live */ +export const deleteLive = async (id: string, options?: RequestInit): Promise => { + const res = await fetch(getDeleteLiveUrl(id), { + ...options, + method: "DELETE" + }); + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: deleteLiveResponse["data"] = body ? JSON.parse(body) : {}; + return { data, status: res.status, headers: res.headers } as deleteLiveResponse; +}; + +export type editLiveResponse200 = { + data: LiveResponseResponse; + status: 200; +}; + +export type editLiveResponse400 = { + data: N400Response; + status: 400; +}; + +export type editLiveResponse401 = { + data: N401Response; + status: 401; +}; + +export type editLiveResponse403 = { + data: N403Response; + status: 403; +}; + +export type editLiveResponse404 = { + data: N404Response; + status: 404; +}; + +export type editLiveResponse405 = { + data: N405Response; + status: 405; +}; + +export type editLiveResponse406 = { + data: N406Response; + status: 406; +}; + +export type editLiveResponse410 = { + data: N410Response; + status: 410; +}; + +export type editLiveResponse429 = { + data: N429Response; + status: 429; +}; + +export type editLiveResponse500 = { + data: N500Response; + status: 500; +}; + +export type editLiveResponse503 = { + data: N503Response; + status: 503; +}; + +export type editLiveResponseSuccess = editLiveResponse200 & { + headers: Headers; +}; +export type editLiveResponseError = ( + | editLiveResponse400 + | editLiveResponse401 + | editLiveResponse403 + | editLiveResponse404 + | editLiveResponse405 + | editLiveResponse406 + | editLiveResponse410 + | editLiveResponse429 + | editLiveResponse500 + | editLiveResponse503 +) & { + headers: Headers; +}; + +export type editLiveResponse = editLiveResponseSuccess | editLiveResponseError; + +export const getEditLiveUrl = (id: string) => { + return `/api/oauth2/v2/lives/${id}`; +}; + +/** @summary Edit live */ +export const editLive = async ( + id: string, + editLiveBody: EditLiveBody, + options?: RequestInit +): Promise => { + const getHeaders = (h?: NonNullable): Record => { + if (!h) return {}; + if (h instanceof Headers) return Object.fromEntries(h.entries()); + if (Array.isArray(h)) return Object.fromEntries(h); + return h; + }; + const res = await fetch(getEditLiveUrl(id), { + ...options, + method: "PATCH", + headers: { "Content-Type": "application/json", ...getHeaders(options?.headers) }, + body: JSON.stringify(editLiveBody) + }); + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: editLiveResponse["data"] = body ? JSON.parse(body) : {}; + return { data, status: res.status, headers: res.headers } as editLiveResponse; +}; + +export type createLiveResponse200 = { + data: LiveResponseResponse; + status: 200; +}; + +export type createLiveResponse400 = { + data: N400Response; + status: 400; +}; + +export type createLiveResponse401 = { + data: N401Response; + status: 401; +}; + +export type createLiveResponse403 = { + data: N403Response; + status: 403; +}; + +export type createLiveResponse404 = { + data: N404Response; + status: 404; +}; + +export type createLiveResponse405 = { + data: N405Response; + status: 405; +}; + +export type createLiveResponse406 = { + data: N406Response; + status: 406; +}; + +export type createLiveResponse410 = { + data: N410Response; + status: 410; +}; + +export type createLiveResponse429 = { + data: N429Response; + status: 429; +}; + +export type createLiveResponse500 = { + data: N500Response; + status: 500; +}; + +export type createLiveResponse503 = { + data: N503Response; + status: 503; +}; + +export type createLiveResponseSuccess = createLiveResponse200 & { + headers: Headers; +}; +export type createLiveResponseError = ( + | createLiveResponse400 + | createLiveResponse401 + | createLiveResponse403 + | createLiveResponse404 + | createLiveResponse405 + | createLiveResponse406 + | createLiveResponse410 + | createLiveResponse429 + | createLiveResponse500 + | createLiveResponse503 +) & { + headers: Headers; +}; + +export type createLiveResponse = createLiveResponseSuccess | createLiveResponseError; + +export const getCreateLiveUrl = () => { + return `/api/oauth2/v2/lives`; +}; + +/** @summary Create live */ +export const createLive = async ( + createLiveBody: CreateLiveBody, + options?: RequestInit +): Promise => { + const getHeaders = (h?: NonNullable): Record => { + if (!h) return {}; + if (h instanceof Headers) return Object.fromEntries(h.entries()); + if (Array.isArray(h)) return Object.fromEntries(h); + return h; + }; + const res = await fetch(getCreateLiveUrl(), { + ...options, + method: "POST", + headers: { "Content-Type": "application/json", ...getHeaders(options?.headers) }, + body: JSON.stringify(createLiveBody) + }); + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: createLiveResponse["data"] = body ? JSON.parse(body) : {}; + return { data, status: res.status, headers: res.headers } as createLiveResponse; +}; + +export type editWebhookResponse200 = { + data: WebhookResponseResponse; + status: 200; +}; + +export type editWebhookResponse400 = { + data: N400Response; + status: 400; +}; + +export type editWebhookResponse401 = { + data: N401Response; + status: 401; +}; + +export type editWebhookResponse403 = { + data: N403Response; + status: 403; +}; + +export type editWebhookResponse404 = { + data: N404Response; + status: 404; +}; + +export type editWebhookResponse405 = { + data: N405Response; + status: 405; +}; + +export type editWebhookResponse406 = { + data: N406Response; + status: 406; +}; + +export type editWebhookResponse410 = { + data: N410Response; + status: 410; +}; + +export type editWebhookResponse429 = { + data: N429Response; + status: 429; +}; + +export type editWebhookResponse500 = { + data: N500Response; + status: 500; +}; + +export type editWebhookResponse503 = { + data: N503Response; + status: 503; +}; + +export type editWebhookResponseSuccess = editWebhookResponse200 & { + headers: Headers; +}; +export type editWebhookResponseError = ( + | editWebhookResponse400 + | editWebhookResponse401 + | editWebhookResponse403 + | editWebhookResponse404 + | editWebhookResponse405 + | editWebhookResponse406 + | editWebhookResponse410 + | editWebhookResponse429 + | editWebhookResponse500 + | editWebhookResponse503 +) & { + headers: Headers; +}; + +export type editWebhookResponse = editWebhookResponseSuccess | editWebhookResponseError; + +export const getEditWebhookUrl = (id: string) => { + return `/api/oauth2/v2/webhooks/${id}`; +}; + +/** @summary Edit webhook */ +export const editWebhook = async ( + id: string, + editWebhookBody: EditWebhookBody, + options?: RequestInit +): Promise => { + const getHeaders = (h?: NonNullable): Record => { + if (!h) return {}; + if (h instanceof Headers) return Object.fromEntries(h.entries()); + if (Array.isArray(h)) return Object.fromEntries(h); + return h; + }; + const res = await fetch(getEditWebhookUrl(id), { + ...options, + method: "PATCH", + headers: { "Content-Type": "application/json", ...getHeaders(options?.headers) }, + body: JSON.stringify(editWebhookBody) + }); + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: editWebhookResponse["data"] = body ? JSON.parse(body) : {}; + return { data, status: res.status, headers: res.headers } as editWebhookResponse; +}; + +export type deleteWebhookResponse204 = { + data: N204Response; + status: 204; +}; + +export type deleteWebhookResponse400 = { + data: N400Response; + status: 400; +}; + +export type deleteWebhookResponse401 = { + data: N401Response; + status: 401; +}; + +export type deleteWebhookResponse403 = { + data: N403Response; + status: 403; +}; + +export type deleteWebhookResponse404 = { + data: N404Response; + status: 404; +}; + +export type deleteWebhookResponse405 = { + data: N405Response; + status: 405; +}; + +export type deleteWebhookResponse406 = { + data: N406Response; + status: 406; +}; + +export type deleteWebhookResponse410 = { + data: N410Response; + status: 410; +}; + +export type deleteWebhookResponse429 = { + data: N429Response; + status: 429; +}; + +export type deleteWebhookResponse500 = { + data: N500Response; + status: 500; +}; + +export type deleteWebhookResponse503 = { + data: N503Response; + status: 503; +}; + +export type deleteWebhookResponseSuccess = deleteWebhookResponse204 & { + headers: Headers; +}; +export type deleteWebhookResponseError = ( + | deleteWebhookResponse400 + | deleteWebhookResponse401 + | deleteWebhookResponse403 + | deleteWebhookResponse404 + | deleteWebhookResponse405 + | deleteWebhookResponse406 + | deleteWebhookResponse410 + | deleteWebhookResponse429 + | deleteWebhookResponse500 + | deleteWebhookResponse503 +) & { + headers: Headers; +}; + +export type deleteWebhookResponse = deleteWebhookResponseSuccess | deleteWebhookResponseError; + +export const getDeleteWebhookUrl = (id: string) => { + return `/api/oauth2/v2/webhooks/${id}`; +}; + +/** @summary Delete webhook */ +export const deleteWebhook = async (id: string, options?: RequestInit): Promise => { + const res = await fetch(getDeleteWebhookUrl(id), { + ...options, + method: "DELETE" + }); + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: deleteWebhookResponse["data"] = body ? JSON.parse(body) : {}; + return { data, status: res.status, headers: res.headers } as deleteWebhookResponse; +}; + +export type getWebhooksResponse200 = { + data: WebhooksResponseResponse; + status: 200; +}; + +export type getWebhooksResponse400 = { + data: N400Response; + status: 400; +}; + +export type getWebhooksResponse401 = { + data: N401Response; + status: 401; +}; + +export type getWebhooksResponse403 = { + data: N403Response; + status: 403; +}; + +export type getWebhooksResponse404 = { + data: N404Response; + status: 404; +}; + +export type getWebhooksResponse405 = { + data: N405Response; + status: 405; +}; + +export type getWebhooksResponse406 = { + data: N406Response; + status: 406; +}; + +export type getWebhooksResponse410 = { + data: N410Response; + status: 410; +}; + +export type getWebhooksResponse429 = { + data: N429Response; + status: 429; +}; + +export type getWebhooksResponse500 = { + data: N500Response; + status: 500; +}; + +export type getWebhooksResponse503 = { + data: N503Response; + status: 503; +}; + +export type getWebhooksResponseSuccess = getWebhooksResponse200 & { + headers: Headers; +}; +export type getWebhooksResponseError = ( + | getWebhooksResponse400 + | getWebhooksResponse401 + | getWebhooksResponse403 + | getWebhooksResponse404 + | getWebhooksResponse405 + | getWebhooksResponse406 + | getWebhooksResponse410 + | getWebhooksResponse429 + | getWebhooksResponse500 + | getWebhooksResponse503 +) & { + headers: Headers; +}; + +export type getWebhooksResponse = getWebhooksResponseSuccess | getWebhooksResponseError; + +export const getGetWebhooksUrl = (params?: GetWebhooksParams) => { + const normalizedParams = new URLSearchParams(); + const deepObjectEntries: string[] = []; + + Object.entries(params || {}).forEach(([key, value]) => { + const deepObjectParameters = ["fields", "page"]; + + if (typeof value === "object" && value !== null && !Array.isArray(value) && deepObjectParameters.includes(key)) { + Object.entries(value).forEach(([subKey, subValue]) => { + if (subValue !== undefined) { + deepObjectEntries.push( + encodeURIComponent(key) + + "[" + + encodeURIComponent(subKey) + + "]=" + + (subValue === null ? "null" : encodeURIComponent(String(subValue))) + ); + } + }); + return; + } + + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : String(value)); + } + }); + + const stringifiedParams = [normalizedParams.toString(), deepObjectEntries.join("&")].filter(Boolean).join("&"); + + return stringifiedParams.length > 0 ? `/api/oauth2/v2/webhooks?${stringifiedParams}` : `/api/oauth2/v2/webhooks`; +}; + +/** @summary Get webhooks */ +export const getWebhooks = async (params?: GetWebhooksParams, options?: RequestInit): Promise => { + const res = await fetch(getGetWebhooksUrl(params), { + ...options, + method: "GET" + }); + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: getWebhooksResponse["data"] = body ? JSON.parse(body) : {}; + return { data, status: res.status, headers: res.headers } as getWebhooksResponse; +}; + +export type createWebhookResponse200 = { + data: WebhooksResponseResponse; + status: 200; +}; + +export type createWebhookResponse400 = { + data: N400Response; + status: 400; +}; + +export type createWebhookResponse401 = { + data: N401Response; + status: 401; +}; + +export type createWebhookResponse403 = { + data: N403Response; + status: 403; +}; + +export type createWebhookResponse404 = { + data: N404Response; + status: 404; +}; + +export type createWebhookResponse405 = { + data: N405Response; + status: 405; +}; + +export type createWebhookResponse406 = { + data: N406Response; + status: 406; +}; + +export type createWebhookResponse410 = { + data: N410Response; + status: 410; +}; + +export type createWebhookResponse429 = { + data: N429Response; + status: 429; +}; + +export type createWebhookResponse500 = { + data: N500Response; + status: 500; +}; + +export type createWebhookResponse503 = { + data: N503Response; + status: 503; +}; + +export type createWebhookResponseSuccess = createWebhookResponse200 & { + headers: Headers; +}; +export type createWebhookResponseError = ( + | createWebhookResponse400 + | createWebhookResponse401 + | createWebhookResponse403 + | createWebhookResponse404 + | createWebhookResponse405 + | createWebhookResponse406 + | createWebhookResponse410 + | createWebhookResponse429 + | createWebhookResponse500 + | createWebhookResponse503 +) & { + headers: Headers; +}; + +export type createWebhookResponse = createWebhookResponseSuccess | createWebhookResponseError; + +export const getCreateWebhookUrl = () => { + return `/api/oauth2/v2/webhooks`; +}; + +/** @summary Create webhook */ +export const createWebhook = async ( + createWebhookBody: CreateWebhookBody, + options?: RequestInit +): Promise => { + const getHeaders = (h?: NonNullable): Record => { + if (!h) return {}; + if (h instanceof Headers) return Object.fromEntries(h.entries()); + if (Array.isArray(h)) return Object.fromEntries(h); + return h; + }; + const res = await fetch(getCreateWebhookUrl(), { + ...options, + method: "POST", + headers: { "Content-Type": "application/json", ...getHeaders(options?.headers) }, + body: JSON.stringify(createWebhookBody) + }); + + const body = [204, 205, 304].includes(res.status) ? null : await res.text(); + + const data: createWebhookResponse["data"] = body ? JSON.parse(body) : {}; + return { data, status: res.status, headers: res.headers } as createWebhookResponse; +}; diff --git a/src/lib/shared/auth-client.ts b/src/lib/shared/auth-client.ts index e178f7dd6..9563e7f6a 100644 --- a/src/lib/shared/auth-client.ts +++ b/src/lib/shared/auth-client.ts @@ -1,6 +1,8 @@ import { env } from "$env/dynamic/public"; +import type { auth } from "$lib/server/auth"; import { apiKeyClient } from "@better-auth/api-key/client"; import { adminClient, organizationClient } from "better-auth/client/plugins"; +import { customSessionClient } from "better-auth/client/plugins"; import { createAuthClient } from "better-auth/svelte"; export const authClient = createAuthClient({ @@ -10,5 +12,5 @@ export const authClient = createAuthClient({ refetchOnWindowFocus: true, refetchWhenOffline: false }, - plugins: [apiKeyClient(), adminClient(), organizationClient()] + plugins: [apiKeyClient(), adminClient(), organizationClient(), customSessionClient()] }); diff --git a/src/lib/shared/profile-emoji.remote.spec.ts b/src/lib/shared/profile-emoji.remote.spec.ts new file mode 100644 index 000000000..4355317f1 --- /dev/null +++ b/src/lib/shared/profile-emoji.remote.spec.ts @@ -0,0 +1,33 @@ +import { beforeEach, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ select: vi.fn(), where: vi.fn() })); +vi.mock("$app/server", () => ({ + query: (_schema: unknown, handler: unknown) => Object.assign(handler as object, { __: { type: "query" } }) +})); +vi.mock("$lib/server/db", () => ({ db: { select: mocks.select } })); + +import { getProfileEmoji } from "./profile-emoji.remote"; + +beforeEach(() => { + vi.resetAllMocks(); + mocks.select.mockReturnValue({ from: () => ({ where: mocks.where }) }); +}); + +it.each([ + [0, "❤️", null], + [1, "🦆", null], + [1, "💙", "💙"], + [1, "❤", "❤️"], + [2, "🦆", "🦆"], + [2, "hello", null], + [2, "🦆🦆", null], + [3, "🦆", null] +])("checks stored entitlement %s before returning %s", async (entitlementLevel, emoji, result) => { + mocks.where.mockResolvedValue([{ emoji, entitlementLevel }]); + await expect(getProfileEmoji("a".repeat(32))).resolves.toBe(result); +}); + +it("returns no emoji when the profile has no permanent grant", async () => { + mocks.where.mockResolvedValue([]); + await expect(getProfileEmoji("a".repeat(32))).resolves.toBeNull(); +}); diff --git a/src/lib/shared/profile-emoji.remote.ts b/src/lib/shared/profile-emoji.remote.ts new file mode 100644 index 000000000..c593c2b1e --- /dev/null +++ b/src/lib/shared/profile-emoji.remote.ts @@ -0,0 +1,15 @@ +import { query } from "$app/server"; +import { db } from "$lib/server/db"; +import { profileEmoji } from "$lib/server/db/schema"; +import { canSelectEmoji, profileEmojiSchema, profileUuidSchema } from "$lib/shared/profile-emoji"; +import { eq } from "drizzle-orm"; + +export const getProfileEmoji = query(profileUuidSchema, async (uuid) => { + const [profile] = await db + .select({ emoji: profileEmoji.emoji, entitlementLevel: profileEmoji.entitlementLevel }) + .from(profileEmoji) + .where(eq(profileEmoji.uuid, uuid)); + if (!profile) return null; + const emoji = profileEmojiSchema.safeParse(profile.emoji); + return emoji.success && canSelectEmoji(profile.entitlementLevel, emoji.data) ? emoji.data : null; +}); diff --git a/src/lib/shared/profile-emoji.spec.ts b/src/lib/shared/profile-emoji.spec.ts new file mode 100644 index 000000000..5c2377719 --- /dev/null +++ b/src/lib/shared/profile-emoji.spec.ts @@ -0,0 +1,26 @@ +import { expect, it } from "vitest"; +import { canSelectEmoji, coloredHearts, profileEmojiSchema } from "./profile-emoji"; + +it.each(["❤️", "❤", "🦆", "🇨🇿", "👩🏽‍💻", "👨‍👩‍👧‍👦", "1️⃣", "🏳️‍🌈", "👍🏽", "☀"])("accepts one emoji: %s", (emoji) => { + expect(profileEmojiSchema.safeParse(emoji).success).toBe(true); +}); + +it.each(["", "hello", "ab", "1", "#", "🦆🦆", "❤️hello", "🇨", "🏽", "🦆\u200b", "🦆\n🔥"])( + "rejects non-emoji input: %s", + (emoji) => { + expect(profileEmojiSchema.safeParse(emoji).success).toBe(false); + } +); + +it("normalizes the legacy heart and enforces permission levels", () => { + expect(profileEmojiSchema.parse(" ❤ ")).toBe("❤️"); + expect(coloredHearts.every((heart) => canSelectEmoji(1, heart))).toBe(true); + expect(canSelectEmoji(0, "❤️")).toBe(false); + expect(canSelectEmoji(1, "🦆")).toBe(false); + expect(canSelectEmoji(2, "🦆")).toBe(true); +}); + +it.each([-1, 0.5, 1.5, 3, Infinity, NaN])("rejects unsupported entitlement level %s", (level) => { + expect(canSelectEmoji(level, "❤️")).toBe(false); + expect(canSelectEmoji(level, "🦆")).toBe(false); +}); diff --git a/src/lib/shared/profile-emoji.ts b/src/lib/shared/profile-emoji.ts new file mode 100644 index 000000000..f85c2ffab --- /dev/null +++ b/src/lib/shared/profile-emoji.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +export const coloredHearts = ["❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍", "🤎", "🩷", "🩵", "🩶"]; + +export const profileUuidSchema = z.string().regex(/^[a-f0-9]{32}$/); +export const profileEmojiSchema = z + .string() + .trim() + .max(32) + .transform((emoji) => (emoji === "❤" ? "❤️" : emoji)) + .refine( + (emoji) => /^(?!\p{Emoji_Component}\uFE0F?$)(?:\p{RGI_Emoji}|\p{Extended_Pictographic}\uFE0F?)$/v.test(emoji), + "Choose a single emoji." + ); + +export function canSelectEmoji(level: number, emoji: string) { + return level === 2 || (level === 1 && coloredHearts.includes(emoji)); +} diff --git a/src/routes/(protected)/dashboard/+layout.svelte b/src/routes/(protected)/dashboard/+layout.svelte new file mode 100644 index 000000000..8a120c24f --- /dev/null +++ b/src/routes/(protected)/dashboard/+layout.svelte @@ -0,0 +1,92 @@ + + + + + + + + +
    +
    + + + + + + {#each generateBreadcrumbs() as breadcrumb, index (breadcrumb.href)} + + + {breadcrumb.name} + + + {#if index < generateBreadcrumbs().length - 1} + + {/if} + {/each} + + +
    + +
    + {#if page.url.searchParams.has("error")} + + Account linking failed + + Account linking could not be completed. Please try linking your account again. + + + {/if} + {#key page.url.pathname} + + {#snippet pending()} +
    + +
    + {/snippet} + {#snippet failed(_error, reset)} + + + Dashboard unavailable + We couldn’t load your account details. Please try again. + + + + + + {/snippet} + {@render children()} +
    + {/key} +
    +
    +
    diff --git a/src/routes/(protected)/dashboard/+page.svelte b/src/routes/(protected)/dashboard/+page.svelte index d7135092e..9865d8255 100644 --- a/src/routes/(protected)/dashboard/+page.svelte +++ b/src/routes/(protected)/dashboard/+page.svelte @@ -1,7 +1,177 @@ -Nothing here yet +
    +
    +
    + {#if discord?.bannerHash} + + + + {:else} +
    + {/if} + +
    + {#if discord?.avatarDecorationHash} + + + + {/if} + + + + + {session?.data?.user?.name?.slice(0, 2).toUpperCase()} + + +
    +
    +
    +
    +
    +

    {session?.data?.user?.name}

    + {#if discord?.primaryGuild && discord?.primaryGuild?.identity_enabled} + + + + {discord.primaryGuild?.tag?.slice(0, 2).toUpperCase()} + + {discord.primaryGuild?.tag} + + {/if} +
    + + {discord?.username} + +
    +

    {discord?.discordUserId}

    +
    +
    +
    +
    + + + + Connected Minecraft accounts + + Manage your Minecraft accounts on + + + + + + {#if minecraft} +
    +
    + {#if needsReconnect} + + {:else} + + {/if} +
    + +
    + {:else} + + + + + + No accounts + + Connect your Minecraft account(s) with to unlock profile specific features and benefits. + + + + + + + {/if} +
    +
    +
    diff --git a/src/routes/(protected)/dashboard/dashboard-sidebar.svelte b/src/routes/(protected)/dashboard/dashboard-sidebar.svelte new file mode 100644 index 000000000..b3aecac5a --- /dev/null +++ b/src/routes/(protected)/dashboard/dashboard-sidebar.svelte @@ -0,0 +1,142 @@ + + + + + + Dashboard + + + {#each items as item (item.title)} + + + {#snippet child({ props })} + + + + {item.title} + + {/snippet} + + + {/each} + + + + + + + {const { user } = $derived(await getDashboard())} + + + + + {#snippet child({ props })} + + + {#if user.image} + + {/if} + {user.name.trim().slice(0, 2).toUpperCase() || "SC"} + +
    + {user.name} +
    + +
    + {/snippet} +
    + + + void signOut()}> + {#if signingOut} + + {:else} + + {/if} + {signingOut ? "Signing out…" : "Sign out"} + + + +
    +
    +
    + {#snippet pending()} + + {/snippet} + {#snippet failed(_error)} + + + + {#snippet child({ props })} + Account settings + {/snippet} + + + + {/snippet} +
    +
    +
    diff --git a/src/routes/(protected)/dashboard/dashboard.remote.spec.ts b/src/routes/(protected)/dashboard/dashboard.remote.spec.ts new file mode 100644 index 000000000..bf4357a3d --- /dev/null +++ b/src/routes/(protected)/dashboard/dashboard.remote.spec.ts @@ -0,0 +1,119 @@ +import { beforeEach, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + event: { + locals: {} as { user?: { id: string; name: string; image: null; createdAt: Date; email: string } }, + request: { headers: new Headers() } + }, + listUserAccounts: vi.fn(), + getStoredPatreonMembership: vi.fn(), + getPatreonVerificationRetryAt: vi.fn(), + verifyPatreonMembership: vi.fn(), + refresh: vi.fn() +})); + +vi.mock("$app/server", () => ({ + getRequestEvent: () => mocks.event, + query: (handler: () => Promise) => + Object.assign(() => Object.assign(handler(), { refresh: mocks.refresh }), { __: { type: "query" } }), + command: (handler: () => unknown) => Object.assign(handler, { __: { type: "command" } }) +})); +vi.mock("$lib/server/auth", () => ({ auth: { api: { listUserAccounts: mocks.listUserAccounts } } })); +vi.mock("$lib/server/patreon", () => ({ + getStoredPatreonMembership: mocks.getStoredPatreonMembership, + getPatreonVerificationRetryAt: mocks.getPatreonVerificationRetryAt, + verifyPatreonMembership: mocks.verifyPatreonMembership, + PatreonVerificationError: class extends Error { + constructor( + message: string, + readonly code: string + ) { + super(message); + } + } +})); + +import { PatreonVerificationError } from "$lib/server/patreon"; +import { getDashboard, verifyMembership } from "./dashboard.remote"; + +beforeEach(() => { + vi.resetAllMocks(); + mocks.event.locals = {}; + mocks.listUserAccounts.mockResolvedValue([]); + mocks.getPatreonVerificationRetryAt.mockResolvedValue(null); +}); + +function signIn() { + mocks.event.locals.user = { + id: "current-user", + name: "Player", + image: null, + createdAt: new Date("2026-01-01"), + email: "private@example.com" + }; +} + +it("requires authentication for dashboard reads and membership verification", async () => { + await expect(getDashboard()).rejects.toMatchObject({ status: 401 }); + await expect(verifyMembership()).rejects.toMatchObject({ status: 401 }); + expect(mocks.listUserAccounts).not.toHaveBeenCalled(); + expect(mocks.verifyPatreonMembership).not.toHaveBeenCalled(); +}); + +it("loads the signed-in user's membership and returns only public account metadata", async () => { + signIn(); + const account = { id: "account", accountId: "provider-user", providerId: "discord", createdAt: new Date() }; + mocks.listUserAccounts.mockResolvedValue([{ ...account, accessToken: "secret", refreshToken: "secret" }]); + mocks.getStoredPatreonMembership.mockResolvedValue({ entitlementLevel: 1 }); + + const result = await getDashboard(); + expect(result).toEqual({ + user: { id: "current-user", name: "Player", image: null, createdAt: new Date("2026-01-01") }, + accounts: [account], + membership: { entitlementLevel: 1 }, + verificationRetryAt: null + }); + expect(mocks.getStoredPatreonMembership).toHaveBeenCalledWith("current-user"); + expect(mocks.listUserAccounts).toHaveBeenCalledWith({ headers: mocks.event.request.headers }); +}); + +it.each([ + ["rate_limited", 429], + ["already_claimed", 409], + ["not_linked", 400] +] as const)("returns the appropriate status for %s", async (code, status) => { + signIn(); + mocks.verifyPatreonMembership.mockRejectedValue(new PatreonVerificationError("Expected failure", code)); + await expect(verifyMembership()).rejects.toMatchObject({ status, body: { message: "Expected failure" } }); + expect(mocks.refresh).not.toHaveBeenCalled(); +}); + +it("hides unexpected provider errors from clients", async () => { + signIn(); + const log = vi.spyOn(console, "error").mockImplementation(() => {}); + mocks.verifyPatreonMembership.mockRejectedValue(new Error("Private provider response")); + try { + await expect(verifyMembership()).rejects.toMatchObject({ + status: 500, + body: { message: "Patreon verification failed unexpectedly." } + }); + expect(mocks.refresh).not.toHaveBeenCalled(); + } finally { + log.mockRestore(); + } +}); + +it("verifies the current user and refreshes dashboard data after success", async () => { + signIn(); + await verifyMembership(); + expect(mocks.verifyPatreonMembership).toHaveBeenCalledWith("current-user"); + expect(mocks.refresh).toHaveBeenCalledOnce(); +}); + +it("returns the in-memory retry time for the current user", async () => { + signIn(); + const retryAt = new Date(Date.now() + 60_000); + mocks.getPatreonVerificationRetryAt.mockResolvedValue(retryAt); + expect((await getDashboard()).verificationRetryAt).toEqual(retryAt); + expect(mocks.getPatreonVerificationRetryAt).toHaveBeenCalledWith("current-user"); +}); diff --git a/src/routes/(protected)/dashboard/dashboard.remote.ts b/src/routes/(protected)/dashboard/dashboard.remote.ts new file mode 100644 index 000000000..ac311912a --- /dev/null +++ b/src/routes/(protected)/dashboard/dashboard.remote.ts @@ -0,0 +1,61 @@ +import { command, getRequestEvent, query } from "$app/server"; +import { auth } from "$lib/server/auth"; +import { + getPatreonVerificationRetryAt, + getStoredPatreonMembership, + PatreonVerificationError, + verifyPatreonMembership +} from "$lib/server/patreon"; +import { getPerks } from "$routes/(protected)/dashboard/donator-perks/perks.remote"; +import { error } from "@sveltejs/kit"; + +export const getDashboard = query(async () => { + const { locals, request } = getRequestEvent(); + if (!locals.user) error(401, "Sign in to view your dashboard."); + + const [accounts, membership, verificationRetryAt] = await Promise.all([ + auth.api.listUserAccounts({ headers: request.headers }), + getStoredPatreonMembership(locals.user.id), + getPatreonVerificationRetryAt(locals.user.id) + ]); + const { id, name, image, createdAt } = locals.user; + + return { + user: { id, name, image, createdAt }, + accounts: accounts.map(({ id, providerId, accountId, createdAt }) => ({ id, providerId, accountId, createdAt })), + membership, + verificationRetryAt + }; +}); + +export const verifyMembership = command(async () => { + const { locals } = getRequestEvent(); + if (!locals.user) error(401, "Sign in before verifying Patreon membership."); + + try { + await verifyPatreonMembership(locals.user.id); + } catch (cause) { + if (cause instanceof PatreonVerificationError) { + let status; + switch (cause.code) { + case "rate_limited": + status = 429; + console.warn("Patreon verification rate limited", cause); + break; + case "already_claimed": + status = 409; + console.warn("Patreon verification already claimed", cause); + break; + default: + status = 400; + console.error("Patreon verification failed", cause); + } + error(status, cause.message); + } + console.error("Unexpected Patreon verification failure", cause); + error(500, "Patreon verification failed unexpectedly."); + } + + await getDashboard().refresh(); + await getPerks().refresh(); +}); diff --git a/src/routes/(protected)/dashboard/dashboard.svelte.spec.ts b/src/routes/(protected)/dashboard/dashboard.svelte.spec.ts new file mode 100644 index 000000000..81c75be97 --- /dev/null +++ b/src/routes/(protected)/dashboard/dashboard.svelte.spec.ts @@ -0,0 +1,70 @@ +import { afterEach, expect, it, vi } from "vitest"; +import { render } from "vitest-browser-svelte"; +import { page, userEvent } from "vitest/browser"; +import Preview from "../../../test-utils/DashboardPreview.svelte"; +import Home from "./+page.svelte"; +import Perks from "./donator-perks/+page.svelte"; +import Settings from "./settings/+page.svelte"; + +const route = vi.hoisted(() => ({ url: new URL("http://localhost/dashboard") })); +vi.mock("$app/state", () => ({ page: route })); +afterEach(() => document.documentElement.classList.remove("dark")); +vi.mock("$lib/shared/auth-client", () => ({ + authClient: { linkSocial: vi.fn(), updateUser: vi.fn(), revokeOtherSessions: vi.fn() } +})); +vi.mock("./dashboard.remote", () => ({ + getDashboard: () => + Promise.resolve({ + user: { id: "preview", name: "SkyBlock Player", createdAt: new Date("2025-01-01") }, + accounts: [], + membership: null + }), + verifyMembership: Object.assign(vi.fn(), { pending: 0 }) +})); +vi.mock("./donator-perks/perks.remote", () => ({ + getPerks: () => Promise.resolve([]), + updateEmoji: vi.fn() +})); +vi.mock("./settings/settings.remote", () => ({ + deleteAccount: vi.fn(), + exportData: vi.fn(), + revokeSession: vi.fn(), + getSessions: () => + Promise.resolve([ + { + id: "preview", + current: true, + userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/130.0.0.0 Safari/537.36", + ipAddress: "127.0.0.1", + createdAt: new Date("2026-01-01"), + expiresAt: new Date("2026-02-01") + } + ]) +})); + +for (const [name, screen, heading] of [ + ["home", Home, "Welcome, SkyBlock Player"], + ["settings", Settings, "Settings"], + ["perks", Perks, "Donator Perks"] +] as const) { + it(`${name} fits desktop and mobile`, async () => { + route.url = new URL( + `http://localhost/dashboard${name === "home" ? "" : name === "perks" ? "/donator-perks" : "/settings"}` + ); + await page.viewport(1280, 1000); + document.documentElement.classList.add("dark"); + const view = await render(Preview, { screen }); + await expect.element(page.getByRole("heading", { name: heading, exact: true })).toBeVisible(); + await page.viewport(390, 844); + await expect.poll(() => document.documentElement.scrollWidth).toBeLessThanOrEqual(390); + await page.getByRole("button", { name: "Toggle Sidebar" }).click(); + await expect.element(page.getByRole("dialog")).toBeVisible(); + await userEvent.keyboard("{Escape}"); + await expect.element(page.getByRole("dialog")).not.toBeInTheDocument(); + if (name === "settings") { + await page.getByText("Browser details", { exact: true }).click(); + await expect.element(page.getByText("Mozilla/5.0", { exact: false })).toBeVisible(); + } + await view.unmount(); + }); +} diff --git a/src/routes/(protected)/dashboard/donator-perks/+page.svelte b/src/routes/(protected)/dashboard/donator-perks/+page.svelte new file mode 100644 index 000000000..fcba43380 --- /dev/null +++ b/src/routes/(protected)/dashboard/donator-perks/+page.svelte @@ -0,0 +1,277 @@ + + +Donator Perks | SkyCrypt + +
    + + +

    Donator Perks

    + Unlock profile emojis as a donator +
    + + + + {#key randomHeartEmoji} + {let currentState = $state("open")} + (currentState = "open")} + onoutrostart={() => (currentState = "closed")} + class="col-start-1 row-start-1 motion-scale-in-80 motion-blur-in-xs data-[state=closed]:motion-scale-out-80 data-[state=closed]:motion-blur-out-xs"> + {randomHeartEmoji} + + {/key} + + + Colored Heart + {money(Number(PUBLIC_PATREON_COLORED_HEART_CENTS))} in total support + + + + + {#key randomEmoji} + {let currentState = $state("open")} + (currentState = "open")} + onoutrostart={() => (currentState = "closed")} + class="col-start-1 row-start-1 motion-scale-in-80 motion-blur-in-xs data-[state=closed]:motion-scale-out-80 data-[state=closed]:motion-blur-out-xs"> + {randomEmoji} + + {/key} + + + Any Emoji + {money(Number(PUBLIC_PATREON_ANY_EMOJI_CENTS))} in total support + + + +
    + + + + Profile Emojis + Manage your profile emojis + + + + {#snippet pending()} + + {/snippet} + {#each await getPerks() as account (account.uuid)} + {#key account.emoji} + + {/key} + {:else} + + + + + Connect your Minecraft account + + You need to connect your Minecraft account to choose your profile emoji. You can connect your + account through the button below. + + + + + {#if minecraftLinked} + + {/if} + + + + + {/each} + + + + + + + Patreon support + Check your lifetime support to unlock your emoji perks. + + + {#if membership} +
    + + + Lifetime support + + {money(membership.lifetimeSupportCents)} /{" "} + {money(Number(PUBLIC_PATREON_ANY_EMOJI_CENTS))} + + +
    + + +
    +
    +
    + +
    +
    +
    +
    + Colored Heart + + + Any Emoji + +
    +
    +
    + {:else} +

    + {#if linked} + Your Patreon account is connected. Your lifetime support determines your emoji perks + {:else} + Already a patron? Link your account to check your support automatically + {/if} +

    + {/if} +
    + + {#if !linked} + + {:else} + + {/if} + + +
    +
    diff --git a/src/routes/(protected)/dashboard/donator-perks/perks.remote.ts b/src/routes/(protected)/dashboard/donator-perks/perks.remote.ts new file mode 100644 index 000000000..048314427 --- /dev/null +++ b/src/routes/(protected)/dashboard/donator-perks/perks.remote.ts @@ -0,0 +1,22 @@ +import { command, getRequestEvent, query } from "$app/server"; +import { getEmojiAccounts, saveProfileEmoji } from "$lib/server/profile-emoji"; +import { profileEmojiSchema, profileUuidSchema } from "$lib/shared/profile-emoji"; +import { getProfileEmoji } from "$lib/shared/profile-emoji.remote"; +import { error } from "@sveltejs/kit"; +import { z } from "zod"; + +export const getPerks = query(async () => { + const { locals } = getRequestEvent(); + if (!locals.user) error(401, "Sign in to view your emoji perks."); + return getEmojiAccounts(locals.user.id); +}); + +export const updateEmoji = command( + z.object({ uuid: profileUuidSchema, emoji: profileEmojiSchema }), + async ({ uuid, emoji }) => { + const { locals } = getRequestEvent(); + if (!locals.user) error(401, "Sign in to change your emoji."); + await saveProfileEmoji(locals.user.id, uuid, emoji); + await Promise.all([getPerks().refresh(), getProfileEmoji(uuid).refresh()]); + } +); diff --git a/src/routes/(protected)/dashboard/donator-perks/perks.svelte.spec.ts b/src/routes/(protected)/dashboard/donator-perks/perks.svelte.spec.ts new file mode 100644 index 000000000..e6b7126a1 --- /dev/null +++ b/src/routes/(protected)/dashboard/donator-perks/perks.svelte.spec.ts @@ -0,0 +1,156 @@ +import { beforeEach, expect, it, vi } from "vitest"; +import { render } from "vitest-browser-svelte"; +import { page } from "vitest/browser"; +import Perks from "./+page.svelte"; + +const mocks = vi.hoisted(() => ({ + retryAt: null as Date | null, + linked: true, + route: { url: new URL("http://localhost/dashboard/donator-perks"), state: {} }, + replaceState: vi.fn(), + linkSocial: vi.fn(), + refresh: vi.fn(), + verify: vi.fn() +})); +vi.mock("$app/state", () => ({ page: mocks.route })); +vi.mock("$app/navigation", () => ({ replaceState: mocks.replaceState })); +beforeEach(() => { + vi.resetAllMocks(); + localStorage.clear(); + mocks.retryAt = null; + mocks.linked = true; + mocks.route.url = new URL("http://localhost/dashboard/donator-perks"); +}); +vi.mock("../dashboard.remote", () => ({ + getDashboard: () => + Object.assign( + Promise.resolve({ + user: { id: "preview" }, + accounts: mocks.linked ? [{ providerId: "patreon" }] : [], + membership: null, + verificationRetryAt: mocks.retryAt + }), + { refresh: mocks.refresh } + ), + verifyMembership: Object.assign(mocks.verify, { pending: 0 }) +})); +vi.mock("./perks.remote", () => ({ + getPerks: () => Object.assign(Promise.resolve([]), { refresh: vi.fn() }), + updateEmoji: vi.fn() +})); +vi.mock("$lib/shared/auth-client", () => ({ authClient: { linkSocial: mocks.linkSocial } })); +vi.mock("svelte-sonner", () => ({ toast: { error: vi.fn(), success: vi.fn() } })); + +it("restores the server cooldown and enables verification when it expires", async () => { + vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] }); + mocks.retryAt = new Date(Date.now() + 2_000); + const view = await render(Perks); + try { + await expect.element(page.getByRole("button", { name: "Check again in 2s" })).toBeDisabled(); + await vi.advanceTimersByTimeAsync(1_000); + await expect.element(page.getByRole("button", { name: "Check again in 1s" })).toBeDisabled(); + await vi.advanceTimersByTimeAsync(1_000); + await expect.element(page.getByRole("button", { name: "Check support", exact: true })).toBeEnabled(); + expect(mocks.verify).not.toHaveBeenCalled(); + } finally { + await view.unmount(); + vi.useRealTimers(); + } +}); + +it("refreshes the cooldown after a failed verification attempt", async () => { + mocks.retryAt = null; + mocks.verify.mockRejectedValueOnce(new Error("Wait one minute before verifying Patreon again.")); + mocks.refresh.mockResolvedValue(undefined); + const view = await render(Perks); + try { + await page.getByRole("button", { name: "Check support", exact: true }).click(); + await expect.poll(() => mocks.refresh.mock.calls.length).toBe(1); + } finally { + await view.unmount(); + } +}); + +it("shows only the link control when Patreon is not connected", async () => { + mocks.linked = false; + mocks.linkSocial.mockResolvedValue({}); + const view = await render(Perks); + try { + await expect.element(page.getByRole("button", { name: "Check support", exact: true })).not.toBeInTheDocument(); + await page.getByRole("button", { name: "Link Patreon", exact: true }).click(); + expect(mocks.linkSocial).toHaveBeenCalledWith({ + provider: "patreon", + callbackURL: "/dashboard/donator-perks?linked=patreon", + errorCallbackURL: "/dashboard/donator-perks" + }); + } finally { + await view.unmount(); + } +}); + +it("checks automatically once after a successful link and hides the link control", async () => { + mocks.route.url = new URL("http://localhost/dashboard/donator-perks?linked=patreon"); + const view = await render(Perks); + try { + await expect.poll(() => mocks.verify.mock.calls.length).toBe(1); + await expect.element(page.getByRole("button", { name: "Link Patreon", exact: true })).not.toBeInTheDocument(); + expect(mocks.replaceState).toHaveBeenCalledWith(new URL("http://localhost/dashboard/donator-perks"), {}); + } finally { + await view.unmount(); + } +}); + +it("waits for the existing cooldown before automatically checking the linked account", async () => { + vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] }); + mocks.retryAt = new Date(Date.now() + 2_000); + mocks.route.url = new URL("http://localhost/dashboard/donator-perks?linked=patreon"); + const view = await render(Perks); + try { + expect(mocks.verify).not.toHaveBeenCalled(); + await vi.advanceTimersByTimeAsync(2_000); + await expect.poll(() => mocks.verify.mock.calls.length).toBe(1); + await vi.advanceTimersByTimeAsync(2_000); + expect(mocks.verify).toHaveBeenCalledOnce(); + } finally { + await view.unmount(); + vi.useRealTimers(); + } +}); + +it("does not automatically check after a failed link", async () => { + mocks.route.url = new URL("http://localhost/dashboard/donator-perks?linked=patreon&error=access_denied"); + const view = await render(Perks); + try { + await expect.element(page.getByRole("button", { name: "Check support", exact: true })).toBeVisible(); + expect(mocks.verify).not.toHaveBeenCalled(); + } finally { + await view.unmount(); + } +}); + +it("restores the localStorage timer without server cooldown state", async () => { + vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] }); + localStorage.setItem("skycryptPatreonRetryTimes", JSON.stringify({ preview: Date.now() + 2_000 })); + const view = await render(Perks); + try { + await expect.element(page.getByRole("button", { name: "Check again in 2s" })).toBeDisabled(); + await vi.advanceTimersByTimeAsync(2_000); + await expect.element(page.getByRole("button", { name: "Check support", exact: true })).toBeEnabled(); + } finally { + await view.unmount(); + vi.useRealTimers(); + } +}); + +it("saves verification cooldowns locally and ignores another user's timer", async () => { + localStorage.setItem("skycryptPatreonRetryTimes", JSON.stringify({ anotherUser: Date.now() + 60_000 })); + const view = await render(Perks); + try { + await page.getByRole("button", { name: "Check support", exact: true }).click(); + const saved = JSON.parse(localStorage.getItem("skycryptPatreonRetryTimes")!); + expect(saved.preview).toBeGreaterThan(Date.now()); + expect(mocks.verify).toHaveBeenCalledOnce(); + } finally { + await view.unmount(); + } +}); diff --git a/src/routes/(protected)/dashboard/minecraft-accounts.svelte b/src/routes/(protected)/dashboard/minecraft-accounts.svelte new file mode 100644 index 000000000..1cf90e762 --- /dev/null +++ b/src/routes/(protected)/dashboard/minecraft-accounts.svelte @@ -0,0 +1,78 @@ + + +
    + {#if accounts.length} + + {#each accounts as account (`${account.accountId}:${account.uuid}`)} + + + + + + {account.username.slice(0, 2).toUpperCase()} + + + + + + + {account.username} + {#if account.primary} + Primary + {/if} + +

    {account.uuid}

    + + + + Synced + + {formatDistanceStrict(account.syncedAt, currentTime, { + addSuffix: true, + in: tz(Intl.DateTimeFormat().resolvedOptions().timeZone) + })} + + +

    + {dateFormat(account.syncedAt, "dd MMMM yyyy 'at' HH:mm", { + in: tz(Intl.DateTimeFormat().resolvedOptions().timeZone) + })} +

    +
    +
    +
    + + +
    + {/each} +
    + {:else} + + + No Minecraft accounts + Sync to import the Minecraft accounts connected to your MC-ID profile + + + {/if} +
    diff --git a/src/routes/(protected)/dashboard/minecraft-accounts.svelte.spec.ts b/src/routes/(protected)/dashboard/minecraft-accounts.svelte.spec.ts new file mode 100644 index 000000000..04d98f813 --- /dev/null +++ b/src/routes/(protected)/dashboard/minecraft-accounts.svelte.spec.ts @@ -0,0 +1,41 @@ +import { beforeEach, expect, it, vi } from "vitest"; +import { render } from "vitest-browser-svelte"; +import { page } from "vitest/browser"; +import MinecraftAccounts from "./minecraft-accounts.svelte"; + +const mocks = vi.hoisted(() => ({ accounts: vi.fn(), sync: Object.assign(vi.fn(), { pending: 0 }), link: vi.fn() })); +vi.mock("./minecraft.remote", () => ({ getMinecraftAccounts: mocks.accounts, syncMinecraftAccounts: mocks.sync })); +vi.mock("$lib/shared/auth-client", () => ({ authClient: { linkSocial: mocks.link } })); +vi.mock("svelte-sonner", () => ({ toast: { success: vi.fn(), error: vi.fn() } })); + +beforeEach(() => { + vi.clearAllMocks(); + mocks.accounts.mockResolvedValue([]); +}); + +it("offers sync for an empty account list and reconnect when authorization expires", async () => { + mocks.sync.mockResolvedValue({ reconnect: true }); + mocks.link.mockResolvedValue({ data: {} }); + await render(MinecraftAccounts); + await expect.element(page.getByText("No Minecraft accounts saved")).toBeVisible(); + await page.getByRole("button", { name: "Sync Minecraft accounts" }).click(); + await page.getByRole("button", { name: "Reconnect MC-ID" }).click(); + expect(mocks.sync).toHaveBeenCalledOnce(); + expect(mocks.link).toHaveBeenCalledWith(expect.objectContaining({ provider: "mc-id", callbackURL: "/dashboard" })); +}); + +it("shows the saved username, UUID, and primary status", async () => { + mocks.accounts.mockResolvedValue([ + { + accountId: "local-account", + uuid: "069a79f444e94726a5befca90e38aaf5", + username: "Notch", + primary: true, + syncedAt: new Date("2026-09-13T12:00:00Z") + } + ]); + await render(MinecraftAccounts); + await expect.element(page.getByText("Notch", { exact: false })).toBeVisible(); + await expect.element(page.getByText("Primary", { exact: true })).toBeVisible(); + await expect.element(page.getByText("069a79f444e94726a5befca90e38aaf5", { exact: true })).toBeVisible(); +}); diff --git a/src/routes/(protected)/dashboard/minecraft.remote.spec.ts b/src/routes/(protected)/dashboard/minecraft.remote.spec.ts new file mode 100644 index 000000000..1ad8495bc --- /dev/null +++ b/src/routes/(protected)/dashboard/minecraft.remote.spec.ts @@ -0,0 +1,75 @@ +import { beforeEach, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + event: { locals: {} as { user?: { id: string } }, request: { headers: new Headers() } }, + getAccessToken: vi.fn(), + getMcIdLinks: vi.fn(), + getStoredMinecraftAccounts: vi.fn(), + fetchMcIdProfile: vi.fn(), + storeMinecraftAccounts: vi.fn(), + refresh: vi.fn() +})); +vi.mock("$app/server", () => ({ + getRequestEvent: () => mocks.event, + query: (handler: () => Promise) => + Object.assign(() => Object.assign(handler(), { refresh: mocks.refresh }), { __: { type: "query" } }), + command: (handler: () => unknown) => Object.assign(handler, { __: { type: "command" } }) +})); +vi.mock("$lib/server/auth", () => ({ auth: { api: { getAccessToken: mocks.getAccessToken } } })); +vi.mock("$lib/server/minecraft-accounts", () => ({ + getMcIdLinks: mocks.getMcIdLinks, + getStoredMinecraftAccounts: mocks.getStoredMinecraftAccounts, + fetchMcIdProfile: mocks.fetchMcIdProfile, + storeMinecraftAccounts: mocks.storeMinecraftAccounts +})); +import { getMinecraftAccounts, syncMinecraftAccounts } from "./minecraft.remote"; + +const link = { id: "local-link", accountId: "provider-subject", userId: "user", providerId: "mc-id" }; +const profile = { sub: link.accountId, accounts: [] }; +beforeEach(() => { + vi.resetAllMocks(); + mocks.event.locals = { user: { id: "user" } }; + mocks.getMcIdLinks.mockResolvedValue([link]); + mocks.getAccessToken.mockResolvedValue({ accessToken: "test-token" }); + mocks.fetchMcIdProfile.mockResolvedValue(profile); +}); + +it("requires authentication before reading or syncing accounts", async () => { + mocks.event.locals = {}; + await expect(getMinecraftAccounts()).rejects.toMatchObject({ status: 401 }); + await expect(syncMinecraftAccounts()).rejects.toMatchObject({ status: 401 }); + expect(mocks.getMcIdLinks).not.toHaveBeenCalled(); + expect(mocks.getStoredMinecraftAccounts).not.toHaveBeenCalled(); +}); + +it("refreshes the current user's local MC-ID link through Better Auth", async () => { + expect(await syncMinecraftAccounts()).toEqual({ reconnect: false }); + expect(mocks.getMcIdLinks).toHaveBeenCalledWith("user"); + expect(mocks.getAccessToken).toHaveBeenCalledWith({ + headers: mocks.event.request.headers, + body: { accountId: link.id } + }); + expect(mocks.fetchMcIdProfile).toHaveBeenCalledWith("test-token"); + expect(mocks.storeMinecraftAccounts).toHaveBeenCalledWith(link, profile); + expect(mocks.refresh).toHaveBeenCalledOnce(); +}); + +it("rejects syncing without a linked MC-ID account", async () => { + mocks.getMcIdLinks.mockResolvedValue([]); + await expect(syncMinecraftAccounts()).rejects.toMatchObject({ status: 400 }); + expect(mocks.getAccessToken).not.toHaveBeenCalled(); +}); + +it.each(["token", "userinfo"])("requests reconnection when %s authorization fails", async (failure) => { + if (failure === "token") mocks.getAccessToken.mockRejectedValue(new Error("Expired")); + else mocks.fetchMcIdProfile.mockResolvedValue(null); + expect(await syncMinecraftAccounts()).toEqual({ reconnect: true }); + expect(mocks.storeMinecraftAccounts).not.toHaveBeenCalled(); +}); + +it("keeps saved data when the provider returns invalid data", async () => { + mocks.fetchMcIdProfile.mockRejectedValue(new Error("Incomplete response")); + await expect(syncMinecraftAccounts()).rejects.toMatchObject({ status: 502 }); + expect(mocks.storeMinecraftAccounts).not.toHaveBeenCalled(); + expect(mocks.refresh).not.toHaveBeenCalled(); +}); diff --git a/src/routes/(protected)/dashboard/minecraft.remote.ts b/src/routes/(protected)/dashboard/minecraft.remote.ts new file mode 100644 index 000000000..238ad9aac --- /dev/null +++ b/src/routes/(protected)/dashboard/minecraft.remote.ts @@ -0,0 +1,39 @@ +import { command, getRequestEvent, query } from "$app/server"; +import { auth } from "$lib/server/auth"; +import { + fetchMcIdProfile, + getMcIdLinks, + getStoredMinecraftAccounts, + storeMinecraftAccounts +} from "$lib/server/minecraft-accounts"; +import { error } from "@sveltejs/kit"; + +export const getMinecraftAccounts = query(async () => { + const { locals } = getRequestEvent(); + if (!locals.user) error(401, "Sign in to view Minecraft accounts."); + return getStoredMinecraftAccounts(locals.user.id); +}); + +export const syncMinecraftAccounts = command(async () => { + const { locals, request } = getRequestEvent(); + if (!locals.user) error(401, "Sign in to sync Minecraft accounts."); + const links = await getMcIdLinks(locals.user.id); + if (!links.length) error(400, "Link MC-ID before syncing Minecraft accounts."); + for (const link of links) { + let accessToken: string; + try { + ({ accessToken } = await auth.api.getAccessToken({ headers: request.headers, body: { accountId: link.id } })); + } catch { + return { reconnect: true }; + } + try { + const profile = await fetchMcIdProfile(accessToken); + if (!profile) return { reconnect: true }; + await storeMinecraftAccounts(link, profile); + } catch { + error(502, "Couldn’t sync Minecraft accounts from MC-ID. Try again or reconnect MC-ID."); + } + } + await getMinecraftAccounts().refresh(); + return { reconnect: false }; +}); diff --git a/src/routes/(protected)/dashboard/settings/+page.svelte b/src/routes/(protected)/dashboard/settings/+page.svelte new file mode 100644 index 000000000..b1f77cc46 --- /dev/null +++ b/src/routes/(protected)/dashboard/settings/+page.svelte @@ -0,0 +1,274 @@ + + + + Account settings | SkyCrypt + + + +
    + + +

    Settings

    + Manage your sessions and personal data. +
    +
    + + + + Where you’re signed in + Check your active sessions. Sign out of any you don’t recognize. + + + + {#each sessions as session (session.id)} + {const device = $derived(sessionDevice(session.userAgent))} + + + {#if device.mobile}{:else}{/if} + + + + {device.name} + {#if session.current}This device{/if} + +

    + Signed in {date(session.createdAt)} · {session.ipAddress || "IP unavailable"} +

    +
    + Browser details +

    + {session.userAgent || "Browser information unavailable"} +

    +

    Session expires {date(session.expiresAt)} (UTC)

    +
    +
    + {#if !session.current} + + + + {/if} +
    + {:else} + + + No active sessions found + Refresh this page to check your sessions again. + + + {/each} +
    +
    + +

    + + {sessions.length === 0 + ? "No active sessions to manage." + : otherSessions.length + ? "You’ll stay signed in here." + : "You’re only signed in here."} +

    + +
    +
    + + + + Export your data + Download a copy of the personal data stored with your SkyCrypt account. + + +

    + Sensitive data such as passwords and API keys are excluded.
    + Public SkyBlock data is managed by Mojang and Hypixel so it is not included in this export. +

    +
    + + + +
    + + + + Delete account + Permanently delete your SkyCrypt account + + +

    + This does not delete your Mojang or Hypixel accounts. You will lose access to your SkyCrypt account and all + associated data. This action cannot be undone. +

    +
    + +

    Export your data first if you want to keep a copy.

    + +
    +
    +
    + + + { + if (deleting) event.preventDefault(); + }} + onEscapeKeydown={(event) => { + if (deleting) event.preventDefault(); + }} + showCloseButton={!deleting}> + + Delete your SkyCrypt account? + + This cannot be undone. Sign in again first if your session is more than a day old. + + +
    + + + Type DELETE to confirm + + {#if deleteError}{deleteError}{/if} + + +
    + + + + +
    +
    diff --git a/src/routes/(protected)/dashboard/settings/session-device.spec.ts b/src/routes/(protected)/dashboard/settings/session-device.spec.ts new file mode 100644 index 000000000..838b90b5b --- /dev/null +++ b/src/routes/(protected)/dashboard/settings/session-device.spec.ts @@ -0,0 +1,15 @@ +import { expect, it } from "vitest"; +import { sessionDevice } from "./session-device"; + +it("distinguishes Chromium browsers and handles mobile and missing metadata", () => { + expect(sessionDevice("Mozilla/5.0 (Windows NT 10.0) Chrome/130 Safari/537.36 Edg/130")).toEqual({ + name: "Edge on Windows", + mobile: false + }); + expect(sessionDevice("Mozilla/5.0 (iPhone; CPU iPhone OS 18_0) CriOS/130 Mobile Safari/604.1")).toEqual({ + name: "Chrome on iPhone", + mobile: true + }); + expect(sessionDevice("Mozilla/5.0 (Macintosh) Version/18.0 Safari/605.1").name).toBe("Safari on macOS"); + expect(sessionDevice(null)).toEqual({ name: "Unknown browser", mobile: false }); +}); diff --git a/src/routes/(protected)/dashboard/settings/session-device.ts b/src/routes/(protected)/dashboard/settings/session-device.ts new file mode 100644 index 000000000..acab9c963 --- /dev/null +++ b/src/routes/(protected)/dashboard/settings/session-device.ts @@ -0,0 +1,28 @@ +export function sessionDevice(userAgent: string | null | undefined) { + const agent = userAgent ?? ""; + const browser = /Edg(?:e|A|iOS)?\//.test(agent) + ? "Edge" + : /(?:OPR|Opera)\//.test(agent) + ? "Opera" + : /(?:Firefox|FxiOS)\//.test(agent) + ? "Firefox" + : /(?:Chrome|CriOS)\//.test(agent) + ? "Chrome" + : /Safari\//.test(agent) + ? "Safari" + : "Unknown browser"; + const platform = /iPad/.test(agent) + ? "iPad" + : /iPhone/.test(agent) + ? "iPhone" + : /Android/.test(agent) + ? "Android" + : /Windows/.test(agent) + ? "Windows" + : /Macintosh|Mac OS X/.test(agent) + ? "macOS" + : /Linux/.test(agent) + ? "Linux" + : null; + return { name: platform ? `${browser} on ${platform}` : browser, mobile: /Android|iPhone|iPad/.test(agent) }; +} diff --git a/src/routes/(protected)/dashboard/settings/settings.remote.spec.ts b/src/routes/(protected)/dashboard/settings/settings.remote.spec.ts new file mode 100644 index 000000000..f9fdf0121 --- /dev/null +++ b/src/routes/(protected)/dashboard/settings/settings.remote.spec.ts @@ -0,0 +1,106 @@ +import { beforeEach, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + event: { request: { headers: new Headers() } }, + getSession: vi.fn(), + listSessions: vi.fn(), + revokeSession: vi.fn(), + signOut: vi.fn(), + exportAccountData: vi.fn(), + deleteAccountData: vi.fn() +})); +vi.mock("$app/server", () => ({ + getRequestEvent: () => mocks.event, + query: (handler: () => unknown) => Object.assign(handler, { __: { type: "query" } }), + command: (schemaOrHandler: object, handler?: (...args: unknown[]) => unknown) => + Object.assign(handler ?? schemaOrHandler, { __: { type: "command" } }) +})); +vi.mock("$lib/server/auth", () => ({ auth: { api: mocks } })); +vi.mock("$lib/server/account-data", () => mocks); +import { deleteAccount, exportData, getSessions, revokeSession } from "./settings.remote"; + +beforeEach(() => { + vi.resetAllMocks(); + mocks.getSession.mockResolvedValue(null); +}); +const signIn = (createdAt = new Date(), impersonatedBy: string | null = null) => + mocks.getSession.mockResolvedValue({ + user: { id: "user" }, + session: { token: "current-secret", createdAt, impersonatedBy } + }); + +it("rejects unauthenticated reads and mutations", async () => { + for (const call of [ + () => getSessions(), + () => exportData(), + () => deleteAccount("DELETE"), + () => revokeSession("other") + ]) { + await expect(call()).rejects.toMatchObject({ status: 401 }); + } + expect(mocks.listSessions).not.toHaveBeenCalled(); + expect(mocks.exportAccountData).not.toHaveBeenCalled(); + expect(mocks.deleteAccountData).not.toHaveBeenCalled(); +}); +it("sorts the current session first without returning authentication tokens", async () => { + signIn(); + const metadata = { + userAgent: "Test browser", + ipAddress: "127.0.0.1", + createdAt: new Date("2026-01-01"), + expiresAt: new Date("2026-01-08") + }; + mocks.listSessions.mockResolvedValue([ + { ...metadata, id: "other", token: "other-secret", userId: "user" }, + { ...metadata, id: "current", token: "current-secret", userId: "user" } + ]); + await expect(getSessions()).resolves.toEqual([ + { ...metadata, id: "current", current: true }, + { ...metadata, id: "other", current: false } + ]); + expect(mocks.getSession).toHaveBeenCalledWith({ + headers: mocks.event.request.headers, + query: { disableCookieCache: true } + }); +}); +it("only revokes another session owned by the signed-in user", async () => { + signIn(); + mocks.listSessions.mockResolvedValue([ + { id: "current", token: "current-secret" }, + { id: "other", token: "other-secret" } + ]); + await expect(revokeSession("stranger")).rejects.toMatchObject({ status: 404 }); + await expect(revokeSession("current")).rejects.toMatchObject({ status: 400 }); + expect(mocks.revokeSession).not.toHaveBeenCalled(); + await revokeSession("other"); + expect(mocks.revokeSession).toHaveBeenCalledWith({ + headers: mocks.event.request.headers, + body: { token: "other-secret" } + }); +}); +it("exports only the authenticated user's data", async () => { + signIn(); + mocks.exportAccountData.mockResolvedValue({ profile: { id: "user" } }); + await expect(exportData()).resolves.toEqual({ profile: { id: "user" } }); + expect(mocks.exportAccountData).toHaveBeenCalledWith("user"); +}); +it("requires a recent session and prevents deletion while impersonating", async () => { + signIn(new Date(Date.now() - 86_400_000)); + await expect(deleteAccount("DELETE")).rejects.toMatchObject({ status: 403 }); + signIn(new Date(), "admin"); + await expect(deleteAccount("DELETE")).rejects.toMatchObject({ status: 403 }); + expect(mocks.deleteAccountData).not.toHaveBeenCalled(); + expect(mocks.signOut).not.toHaveBeenCalled(); +}); +it("deletes the authenticated account and clears its sign-in cookies", async () => { + signIn(); + await deleteAccount("DELETE"); + expect(mocks.deleteAccountData).toHaveBeenCalledWith("user"); + expect(mocks.signOut).toHaveBeenCalledWith({ headers: mocks.event.request.headers }); +}); +it("does not report success or sign out if deletion fails", async () => { + signIn(); + mocks.deleteAccountData.mockRejectedValue(new Error("Database unavailable")); + await expect(deleteAccount("DELETE")).rejects.toThrow("Database unavailable"); + expect(mocks.signOut).not.toHaveBeenCalled(); +}); diff --git a/src/routes/(protected)/dashboard/settings/settings.remote.ts b/src/routes/(protected)/dashboard/settings/settings.remote.ts new file mode 100644 index 000000000..a0ff41353 --- /dev/null +++ b/src/routes/(protected)/dashboard/settings/settings.remote.ts @@ -0,0 +1,55 @@ +import { command, getRequestEvent, query } from "$app/server"; +import { deleteAccountData, exportAccountData } from "$lib/server/account-data"; +import { auth } from "$lib/server/auth"; +import { error } from "@sveltejs/kit"; +import { z } from "zod"; + +async function requireSession() { + const { request } = getRequestEvent(); + const session = await auth.api.getSession({ headers: request.headers, query: { disableCookieCache: true } }); + if (!session) error(401, "Sign in to manage your account."); + return { ...session, headers: request.headers }; +} + +export const getSessions = query(async () => { + const { session, headers } = await requireSession(); + const sessions = await auth.api.listSessions({ headers }); + return sessions + .map(({ id, token, userAgent, ipAddress, createdAt, expiresAt }) => ({ + id, + current: token === session.token, + userAgent, + ipAddress, + createdAt, + expiresAt + })) + .sort( + (a, b) => + Number(b.current) - Number(a.current) || new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + ); +}); + +export const revokeSession = command(z.string().min(1), async (id) => { + const { session, headers } = await requireSession(); + const sessions = await auth.api.listSessions({ headers }); + const target = sessions.find((item) => item.id === id); + if (!target) error(404, "Session not found."); + if (target.token === session.token) error(400, "Use the account menu to sign out of this session."); + await auth.api.revokeSession({ headers, body: { token: target.token } }); +}); + +export const exportData = command(async () => { + const { user } = await requireSession(); + return exportAccountData(user.id); +}); + +export const deleteAccount = command(z.literal("DELETE"), async () => { + const { user, session, headers } = await requireSession(); + // Match Better Auth's default one-day freshness requirement for destructive account changes. + if (Date.now() - new Date(session.createdAt).getTime() >= 86_400_000) { + error(403, "Sign out and sign in again before deleting your account."); + } + if (session.impersonatedBy) error(403, "You cannot delete an account while impersonating its owner."); + await deleteAccountData(user.id); + await auth.api.signOut({ headers }); +}); diff --git a/src/routes/(protected)/dashboard/settings/settings.svelte.spec.ts b/src/routes/(protected)/dashboard/settings/settings.svelte.spec.ts new file mode 100644 index 000000000..f47e09e62 --- /dev/null +++ b/src/routes/(protected)/dashboard/settings/settings.svelte.spec.ts @@ -0,0 +1,156 @@ +import { afterEach, beforeEach, expect, it, vi } from "vitest"; +import { render } from "vitest-browser-svelte"; +import { page, userEvent } from "vitest/browser"; +import Preview from "../../../../test-utils/DashboardPreview.svelte"; +import Settings from "./+page.svelte"; + +const mocks = vi.hoisted(() => ({ + refresh: vi.fn(), + revoke: vi.fn(), + revokeOther: vi.fn(), + exportData: vi.fn(), + deleteAccount: vi.fn(), + signOut: vi.fn(), + getDashboard: vi.fn() +})); +vi.mock("$app/state", () => ({ page: { url: new URL("http://localhost/dashboard/settings") } })); +vi.mock("$lib/shared/auth-client", () => ({ + authClient: { revokeOtherSessions: mocks.revokeOther, signOut: mocks.signOut } +})); +vi.mock("../dashboard.remote", () => ({ getDashboard: mocks.getDashboard })); +vi.mock("./settings.remote", () => ({ + getSessions: () => + Object.assign( + Promise.resolve([ + { + id: "current", + current: true, + userAgent: "Mozilla/5.0 (Macintosh) Chrome/130 Safari/537.36", + ipAddress: "127.0.0.1", + createdAt: new Date("2026-01-01"), + expiresAt: new Date("2026-02-01") + }, + { + id: "other", + current: false, + userAgent: "Mozilla/5.0 (iPhone) Version/18 Safari/604.1", + ipAddress: "2001:db8::1", + createdAt: new Date("2026-01-02"), + expiresAt: new Date("2026-02-02") + } + ]), + { refresh: mocks.refresh } + ), + revokeSession: mocks.revoke, + exportData: mocks.exportData, + deleteAccount: mocks.deleteAccount +})); +beforeEach(() => { + vi.clearAllMocks(); + mocks.getDashboard.mockResolvedValue({ user: { name: "SkyBlock Player" } }); +}); +afterEach(() => document.documentElement.classList.remove("dark")); + +it("removes profile editing and handles individual sign-out on mobile", async () => { + await page.viewport(390, 844); + await render(Preview, { screen: Settings }); + await expect.element(page.getByRole("heading", { name: "Settings", exact: true })).toBeVisible(); + await expect.element(page.getByRole("textbox", { name: "Display name" })).not.toBeInTheDocument(); + await expect.element(page.getByText("Chrome on macOS", { exact: false })).toBeVisible(); + await expect.element(page.getByText("Safari on iPhone", { exact: true })).toBeVisible(); + await page.getByRole("button", { name: "Sign out of Safari on iPhone" }).click(); + expect(mocks.revoke).toHaveBeenCalledWith("other"); + expect(mocks.refresh).toHaveBeenCalled(); + await expect.poll(() => document.documentElement.scrollWidth).toBeLessThanOrEqual(390); +}); +it("requires typed confirmation, keeps deletion errors visible, and supports cancellation", async () => { + mocks.deleteAccount.mockRejectedValue(new Error("Sign in again before deleting your account.")); + await render(Preview, { screen: Settings }); + await page.getByRole("button", { name: "Delete account", exact: true }).click(); + const confirm = page.getByRole("button", { name: "Permanently delete account" }); + await expect.element(confirm).toBeDisabled(); + await page.getByRole("textbox", { name: "Type DELETE to confirm" }).fill("DELETE"); + await expect.element(confirm).toBeEnabled(); + await confirm.click(); + expect(mocks.deleteAccount).toHaveBeenCalledWith("DELETE"); + await expect.element(page.getByRole("alert")).toHaveTextContent("Sign in again before deleting your account."); + await userEvent.keyboard("{Escape}"); + await expect.element(page.getByRole("dialog")).not.toBeInTheDocument(); + await page.getByRole("button", { name: "Delete account", exact: true }).click(); + await expect.element(confirm).toBeDisabled(); + await page.getByRole("button", { name: "Cancel", exact: true }).click(); + await expect.element(page.getByRole("dialog")).not.toBeInTheDocument(); +}); +it("allows retrying a failed export", async () => { + mocks.exportData.mockRejectedValue(new Error("Offline")); + await render(Preview, { screen: Settings }); + const download = page.getByRole("button", { name: "Download your data" }); + await download.click(); + expect(mocks.exportData).toHaveBeenCalledTimes(1); + await expect.element(download).toBeEnabled(); +}); + +it("fits light and dark themes on desktop and mobile", async () => { + await page.viewport(1280, 1000); + await render(Preview, { screen: Settings }); + await expect.element(page.getByRole("heading", { name: "Settings", exact: true })).toBeVisible(); + await expect.poll(() => document.documentElement.scrollWidth).toBeLessThanOrEqual(1280); + document.documentElement.classList.add("dark"); + await page.viewport(390, 844); + await expect.poll(() => document.documentElement.scrollWidth).toBeLessThanOrEqual(390); +}); + +it("opens the account menu in collapsed and mobile sidebars and allows retrying sign-out", async () => { + await page.viewport(1280, 1000); + mocks.signOut.mockResolvedValue({ error: { message: "Unable to sign out." } }); + await render(Preview, { screen: Settings }); + const accountMenu = page.getByRole("button", { name: "Account menu for SkyBlock Player" }); + await expect.element(accountMenu).toBeVisible(); + await expect + .poll(() => { + const footer = document.querySelector('[data-sidebar="footer"]'); + return footer?.getBoundingClientRect().bottom ?? Infinity; + }) + .toBeLessThanOrEqual(window.innerHeight); + await page.getByRole("button", { name: "Toggle Sidebar" }).click(); + await accountMenu.click(); + await expect + .element(page.getByRole("menuitem", { name: "Account settings" })) + .toHaveAttribute("href", "/dashboard/settings"); + await userEvent.keyboard("{Escape}"); + await expect.element(page.getByRole("menu")).not.toBeInTheDocument(); + await accountMenu.click(); + await page.getByRole("menuitem", { name: "Sign out", exact: true }).click(); + expect(mocks.signOut).toHaveBeenCalledTimes(1); + await expect.element(accountMenu).toBeEnabled(); + await page.viewport(390, 844); + await page.getByRole("button", { name: "Toggle Sidebar" }).click(); + await expect.element(page.getByRole("dialog")).toBeVisible(); + await accountMenu.click(); + await expect.element(page.getByRole("menuitem", { name: "Account settings" })).toBeVisible(); + await userEvent.keyboard("{Escape}"); + await expect.element(page.getByRole("menu")).not.toBeInTheDocument(); + await userEvent.keyboard("{Escape}"); + await expect.element(page.getByRole("dialog")).not.toBeInTheDocument(); +}); + +it("keeps navigation available while the account boundary is pending", async () => { + await page.viewport(1280, 1000); + const account = Promise.withResolvers<{ user: { name: string } }>(); + mocks.getDashboard.mockReturnValue(account.promise); + await render(Preview, { screen: Settings }); + await expect.element(page.getByRole("status", { name: "Loading account menu" })).toBeVisible(); + await expect.element(page.getByRole("link", { name: "Home", exact: true })).toBeVisible(); + account.resolve({ user: { name: "SkyBlock Player" } }); + await expect.element(page.getByRole("button", { name: "Account menu for SkyBlock Player" })).toBeVisible(); + await expect.element(page.getByRole("status", { name: "Loading account menu" })).not.toBeInTheDocument(); +}); + +it("contains an account query failure in the footer boundary", async () => { + await page.viewport(1280, 1000); + mocks.getDashboard.mockRejectedValue(new Error("Account unavailable")); + await render(Preview, { screen: Settings }); + await expect.element(page.getByRole("link", { name: "Account settings", exact: true })).toBeVisible(); + await expect.element(page.getByRole("link", { name: "Home", exact: true })).toBeVisible(); + await expect.element(page.getByRole("heading", { name: "Settings", exact: true })).toBeVisible(); +}); diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte index 859f5f7e1..ffcad60a1 100644 --- a/src/routes/login/+page.svelte +++ b/src/routes/login/+page.svelte @@ -1,58 +1,69 @@
    -
    -
    + class="@container mx-auto flex min-h-[calc(100dvh-3rem)] w-full max-w-md flex-col items-center-safe justify-center-safe pr-[max(1.25rem,env(safe-area-inset-right))] pb-[max(2.5rem,env(safe-area-inset-bottom))] pl-[max(1.25rem,env(safe-area-inset-left))]"> + + - SC + class="pointer-events-none size-16 rounded-xl" /> + SC -
    -

    Sign in to SkyCrypt

    -
    -
    - -
    - - - - - {#if signInSocial.pending} - -
    + Sign in to SkyCrypt + + + -

    - We only request your basic Discord profile (username, avatar, and banner).
    We do not access your email or - any other personal information.

    If this is not the case, then you may be a victim of a phishing attempt. -

    -
    + + + + + + + + + We only request your basic Discord profile (username, avatar, and banner).
    We do not access your email + or any other personal information.

    If this is not the case, then you may be a victim of a phishing + attempt. +
    +
    +
    +
    diff --git a/src/routes/login/login.remote.ts b/src/routes/login/login.remote.ts deleted file mode 100644 index ce26180bd..000000000 --- a/src/routes/login/login.remote.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { form } from "$app/server"; -import { auth } from "$src/lib/server/auth"; -import { fail, redirect } from "@sveltejs/kit"; -import { z } from "zod"; - -export const signInSocial = form( - z.object({ provider: z.string(), callbackURL: z.string() }), - async ({ provider, callbackURL }) => { - if (!provider) { - return fail(400, { message: "Provider is required" }); - } - - const result = await auth.api.signInSocial({ - body: { - provider: provider, - callbackURL - } - }); - - if (result.url) { - return redirect(302, result.url); - } - return fail(400, { message: "Social sign-in failed" }); - } -); diff --git a/src/test-utils/EmojiPickerTest.svelte b/src/test-utils/EmojiPickerTest.svelte new file mode 100644 index 000000000..7d216fbd4 --- /dev/null +++ b/src/test-utils/EmojiPickerTest.svelte @@ -0,0 +1,42 @@ + + +{#snippet picker()} + + + + + {#snippet children({ active })} +
    {active?.emoji} {active?.data.name}
    + {/snippet} +
    + +
    +{/snippet} + +{#if inPopover} + + Open picker + {@render picker()} + +{:else} + {@render picker()} +{/if} diff --git a/vite.config.ts b/vite.config.ts index 70dbdd91c..f6612f9e1 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -69,7 +69,8 @@ export default defineConfig({ "https://nmsr.nickac.dev", "https://cms.shiiyu.moe", "http://localhost:3000", - "https://eliteskyblock.com" + "https://eliteskyblock.com", + "https://cdn.discordapp.com" ], "connect-src": [ "self", From 1e492c47eca5bcca69280016cfb0091daaf3e629 Mon Sep 17 00:00:00 2001 From: DarthGigi <47110839+DarthGigi@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:31:46 +0200 Subject: [PATCH 031/135] chore(ui): refresh shared components and generated assets --- AGENTS.md | 101 +++- src/lib/components/EmptyStat.svelte | 4 +- .../components/dashboard/LinkAccount.svelte | 42 ++ src/lib/components/header/Menu.svelte | 4 +- src/lib/components/misc/SearchTabs.svelte | 4 +- src/lib/components/notices/BetaNotice.svelte | 2 +- .../ui/breadcrumb/breadcrumb-ellipsis.svelte | 33 +- .../ui/breadcrumb/breadcrumb-item.svelte | 22 +- .../ui/breadcrumb/breadcrumb-link.svelte | 46 +- .../ui/breadcrumb/breadcrumb-list.svelte | 22 +- .../ui/breadcrumb/breadcrumb-page.svelte | 33 +- .../ui/breadcrumb/breadcrumb-separator.svelte | 36 +- .../ui/breadcrumb/breadcrumb.svelte | 28 +- src/lib/components/ui/breadcrumb/index.ts | 30 +- src/lib/hooks/is-mobile.svelte.ts | 7 +- src/lib/shared/api/cms-generated.ts | 432 ++++++++++++++++-- src/test-utils/DashboardPreview.svelte | 9 + 17 files changed, 636 insertions(+), 219 deletions(-) create mode 100644 src/lib/components/dashboard/LinkAccount.svelte create mode 100644 src/test-utils/DashboardPreview.svelte diff --git a/AGENTS.md b/AGENTS.md index 41daf1725..c1610aaee 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,37 +1,92 @@ -# Repository Guidelines +# AGENTS.md -## Project Structure & Module Organization +Follow these instructions when working on SkyCrypt Frontend. Nested `AGENTS.md` files may add instructions for their directories. See `CONTRIBUTING.md` for setup and release policy. -SkyCrypt is a SvelteKit 2 application built with TypeScript and Svelte 5. Application code lives in `src/`: reusable UI belongs in `lib/components`, structural composition in `lib/layouts`, domain features in `lib/sections`, utilities and API clients in `lib/shared`, server-only code in `lib/server`, and reactive contexts in `context`. Routes live in `src/routes`, while static assets belong in `static/`. Colocate tests with implementation files; place shared test helpers in `src/test-utils`. +## Working in this repository -Follow the `components -> layouts -> sections` layering. Initialize global contexts in `src/routes/+layout.svelte`. Do not edit Orval-generated API files manually; regenerate them with the configured generator. +- Read the code, configuration, and tests relevant to the task before editing. Preserve unrelated staged and unstaged work. +- Reuse existing helpers and components before introducing dependencies or abstractions. +- Update this file when project practices change. Check instructions against the repository and link to existing documentation instead of repeating it. Keep machine-specific paths out of this file. -## Build, Test, and Development Commands +## Stack and setup -Use Node 24 and pnpm 11; other package managers are unsupported. +- SkyCrypt is a Hypixel SkyBlock profile viewer built with SvelteKit 2, Svelte 5, strict TypeScript, and Tailwind CSS 4. Production uses the Node adapter. +- Use Node 24 and pnpm 12, as required by `package.json` and CI. Other runtimes and package managers are unsupported. Keep `pnpm-lock.yaml` in sync with dependency changes. +- Install with `pnpm install`. Copy `.env.example` to `.env` without overwriting an existing file. Never commit secrets or local environment files. +- Profile data and end-to-end tests require the Go backend from the separate SkyCrypt-Backend repository. Newsroom features use a separate CMS. See `.env.example` for service URLs. +- Database and authentication code use PostgreSQL, Drizzle, and Better Auth. `pnpm db:start` starts local PostgreSQL. Server startup applies migrations from `drizzle/` through `src/hooks.server.ts`; builds skip migration execution. -- `pnpm dev`: start the Vite development server. -- `pnpm build`: create a production build. -- `pnpm preview`: serve the production build locally. -- `pnpm check`: run Svelte and TypeScript diagnostics. -- `pnpm lint`: run oxfmt, ESLint, and correctness checks. -- `pnpm test`: run Vitest once. -- `pnpm test:coverage`: run tests with V8 coverage. -- `pnpm test:e2e`: run Playwright end-to-end tests. -- `pnpm format`: apply repository formatting. +## Structure and boundaries -## Coding Style & Naming Conventions +| Location | Purpose | +| --------------------- | ---------------------------------------------------------------------- | +| `src/lib/components/` | Reusable UI and feature components; shared UI primitives live in `ui/` | +| `src/lib/layouts/` | Structural composition of components | +| `src/lib/sections/` | Domain sections composed from layouts and components | +| `src/lib/shared/` | Shared utilities, themes, API clients, and remote functions | +| `src/lib/server/` | Server-only authentication, integrations, and database code | +| `src/lib/hooks/` | Reusable Svelte hooks | +| `src/lib/types/` | Shared TypeScript types | +| `src/context/` | Reactive contexts and preferences | +| `src/routes/` | SvelteKit routes and route-specific remote functions | +| `src/test-utils/` | Shared test wrappers and helpers | +| `e2e/` | Playwright user-flow tests | +| `drizzle/` | SQL migrations and migration metadata | +| `static/` | Public assets | +| `scripts/` | Release and maintenance scripts | -oxfmt enforces two-space indentation, double quotes, no trailing commas, and ordered Tailwind classes. Keep TypeScript strict and prefer aliases such as `$lib`, `$ctx`, and `$types`. Use Svelte 5 runes (`$state`, `$derived`, `$props`) and snippets instead of legacy reactive declarations, `export let`, or slots. Name Svelte components in PascalCase, utilities in camelCase, and route files using SvelteKit conventions. Prefer Tailwind v4 utilities and the shared `cn()` helper. +- Compose components into layouts, then layouts into sections. Components must not depend on sections. +- Initialize global contexts in `src/routes/+layout.svelte`. Keep request-specific user and session state out of shared server globals. +- Use configured aliases such as `$lib`, `$ctx`, `$types`, `$ui`, and `$utils`. Alias and SvelteKit configuration live in `vite.config.ts`. +- Keep credentials and database access in server-only code. Treat every `PUBLIC_*` environment variable as exposed to the browser. -## Testing Guidelines +## Implementation conventions -Write focused Vitest tests beside the code. Use `*.spec.ts` for Node tests and `*.svelte.spec.ts` for browser/component tests. Add regression coverage for behavior changes and use Playwright for complete user flows. Run `pnpm lint` and relevant tests before submitting changes. +- Use Svelte 5 runes (`$state`, `$derived`, `$props`) and snippets. Avoid legacy reactive declarations, `export let`, and slots in new code. +- Use the Svelte MCP to get the latest svelte documentation and conventions. Use the async flag to get the latest SvelteKit documentation and conventions for async svelte and remote functions, as that is the default for this project (instead of +page.server.ts way of loading data). +- The project enables async Svelte compilation and SvelteKit remote functions. Use `--async` when validating with Svelte MCP/CLI tooling. +- Use SvelteKit remote functions in `*.remote.ts` for new data loading and mutations instead of adding `+page.server.ts`. Migrate existing server route files only when the task calls for it. +- Validate remote-function inputs and check authentication and authorization on the server. Follow nearby implementations, and check permissions in each protected mutation even when route guards or hidden controls restrict access. +- Use Tailwind utilities, the shared `cn()` helper, and existing theme tokens and UI primitives. UI changes must work with a keyboard, show focus, adapt to screen size, and support existing themes. +- Name reusable Svelte components in PascalCase and utilities in camelCase; follow SvelteKit route naming and nearby feature-file conventions. +- Follow `.oxfmtrc.json`: two spaces, double quotes, no trailing commas, sorted imports, and ordered Tailwind classes. Format only changed files. -## Commit & Pull Request Guidelines +## Generated code and database changes -Use Conventional Commits, for example `feat(newsroom): add post filters` or `fix(theme): preserve contrast`. Keep commits scoped and intentional. Pull requests should explain the problem and solution, link relevant issues, list verification commands, and include before/after screenshots for visible UI changes. Add a changeset for user-facing release changes when appropriate. +- Do not manually edit Orval-generated API clients or schemas in `src/lib/shared/api/`. Change `orval.config.ts`, custom mutators, or the upstream API definition, then regenerate the relevant target. +- Run `pnpm exec orval --project ` for `skycrypt`, `skycryptZod`, `cms`, or `patreon`. SkyCrypt targets read `http://localhost:8080/api/openapi/doc.json`; CMS reads `http://localhost:3000/api/openapi.json`; Patreon reads its configured upstream spec. Required services must be available. +- Edit database schemas in `src/lib/server/db/schema/`, then run `pnpm db:generate` and review the SQL and metadata in `drizzle/`. Do not rewrite applied migrations. +- `pnpm auth:schema` regenerates the Better Auth schema from `src/lib/server/auth.ts`. Review the generated changes and generate any required database migration separately. +- `pnpm db:push` directly changes the configured database; use it only for local schema work. Commit migrations for schema changes. Drizzle CLI commands require `DATABASE_URL` in the process environment. -## Configuration & Security +## Commands and verification -Copy `.env.example` for local configuration and never commit secrets. Keep credentials in server-only modules; expose client values only when intentionally configured as public. +| Command | Purpose | +| ---------------------------------------------- | ----------------------------------------------------------------- | +| `pnpm dev` | Start Vite at `http://localhost:5173` | +| `pnpm check` | Synchronize SvelteKit types and run Svelte/TypeScript diagnostics | +| `pnpm lint` | Check formatting, run ESLint, and run `pnpm check` | +| `pnpm exec oxfmt --write ` | Format selected files | +| `pnpm format` | Format the repository | +| `pnpm test` | Run both Vitest projects once | +| `pnpm exec vitest run --project server ` | Run a focused Node test | +| `pnpm exec vitest run --project client ` | Run a focused browser/component test | +| `pnpm test:coverage` | Run Vitest with V8 coverage | +| `pnpm test:e2e` | Run Playwright tests; starts or reuses the local dev server | +| `pnpm build` | Create the production build | +| `pnpm preview` | Serve the build locally at `http://localhost:4173` | +| `pnpm runbuild` | Start the built Node server | + +- Add regression tests beside the code when behavior changes. Use `*.spec.ts` for Node tests and `*.svelte.spec.ts` for browser/component tests. Reuse helpers from `src/test-utils/`. +- Vitest has `server` and `client` projects in `vite.config.ts`. The client project uses headless Chromium and requires `pnpm exec playwright install chromium`. Tests must make assertions; `expect.requireAssertions` is enabled. +- Run `pnpm lint`, relevant tests, and `pnpm build` for code changes before submitting. For documentation-only changes, check formatting and the accuracy of referenced paths and commands. +- End-to-end tests run locally with the backend and its required API credentials; they are currently disabled in CI. A production build can also require backend access for prerendered data. +- Report which checks you ran, their results, and anything you could not run. Never report a skipped check as passing. + +## Commits, pull requests, and releases + +- Use Conventional Commits, for example `fix(theme): preserve contrast`. The commit-message hook runs commitlint. +- Follow `.github/PULL_REQUEST_TEMPLATE.md`: explain the problem and resulting behavior, link relevant issues, list verification, and include before/after screenshots for visible UI changes. Note migration or environment requirements. +- Add a changeset with `pnpm changeset` when the change belongs in the release changelog. Documentation-only agent guidance does not need one. +- Changesets target `dev`. Automation creates beta releases on `dev` and stable releases on `prod`. Follow `CONTRIBUTING.md` for promotion and changeset cleanup. Run release scripts only for an explicit release task. +- Preserve applicable license notices. New contributions default to AGPLv3; brand assets explicitly listed in `NOTICE` and `static/BRAND_ASSETS_LICENSE` retain their MIT exception. diff --git a/src/lib/components/EmptyStat.svelte b/src/lib/components/EmptyStat.svelte index 8df73bbfa..babd64236 100644 --- a/src/lib/components/EmptyStat.svelte +++ b/src/lib/components/EmptyStat.svelte @@ -1,12 +1,12 @@ + + diff --git a/src/lib/components/header/Menu.svelte b/src/lib/components/header/Menu.svelte index f189f6b99..c0236b7a2 100644 --- a/src/lib/components/header/Menu.svelte +++ b/src/lib/components/header/Menu.svelte @@ -8,7 +8,7 @@ import { cn } from "$src/lib/shared/utils"; import * as Kbd from "$ui/kbd"; import * as NavigationMenu from "$ui/navigation-menu"; - import { type Icon as IconType } from "@lucide/svelte"; + import type { LucideIcon } from "@lucide/svelte"; import NewspaperIcon from "@lucide/svelte/icons/newspaper"; import SearchIcon from "@lucide/svelte/icons/search"; import { SvglDiscordLogo, SvglPatreonLogo, type SvglLogo } from "@selemondev/svgl-svelte"; @@ -18,7 +18,7 @@ title: string; href: string; content: string; - icon: typeof IconType | typeof SvglLogo; + icon: LucideIcon | typeof SvglLogo; }; const { PUBLIC_DISCORD_INVITE, PUBLIC_PATREON } = env; diff --git a/src/lib/components/misc/SearchTabs.svelte b/src/lib/components/misc/SearchTabs.svelte index 48eb5eff6..b753cf735 100644 --- a/src/lib/components/misc/SearchTabs.svelte +++ b/src/lib/components/misc/SearchTabs.svelte @@ -6,7 +6,7 @@ import ScrollArea from "$src/lib/components/ui/scroll-area/scroll-area.svelte"; import { Input } from "$ui/input"; import * as Tabs from "$ui/tabs"; - import { type Icon as IconType } from "@lucide/svelte"; + import type { LucideIcon } from "@lucide/svelte"; import SearchXIcon from "@lucide/svelte/icons/search-x"; import type { Snippet } from "svelte"; import { cubicOut } from "svelte/easing"; @@ -33,7 +33,7 @@ viewportClasses?: string; emptyTitle?: string; emptyDescription?: string; - emptyIcon?: typeof IconType; + emptyIcon?: LucideIcon; noResultsLabel?: string; }; diff --git a/src/lib/components/notices/BetaNotice.svelte b/src/lib/components/notices/BetaNotice.svelte index 8ede4f3eb..408c7fe46 100644 --- a/src/lib/components/notices/BetaNotice.svelte +++ b/src/lib/components/notices/BetaNotice.svelte @@ -10,7 +10,7 @@