diff --git a/apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx b/apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx index 26ec7baf3..ded17d62c 100644 --- a/apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx +++ b/apps/dashboard/app/(main)/websites/[id]/flags/_components/flag-sheet.tsx @@ -248,6 +248,7 @@ export function FlagSheet({ dependencies: [], environment: undefined, targetGroupIds: [], + folder: undefined, }, schedule: undefined, }, @@ -290,6 +291,7 @@ export function FlagSheet({ dependencies: flag.dependencies ?? [], environment: flag.environment || undefined, targetGroupIds: extractTargetGroupIds(), + folder: flag.folder || undefined, }, schedule: undefined, }); @@ -311,6 +313,7 @@ export function FlagSheet({ variants: template.type === "multivariant" ? template.variants : [], dependencies: [], targetGroupIds: [], + folder: undefined, }, schedule: undefined, }); @@ -332,6 +335,7 @@ export function FlagSheet({ variants: [], dependencies: [], targetGroupIds: [], + folder: undefined, }, schedule: undefined, }); @@ -549,6 +553,32 @@ export function FlagSheet({ )} /> + + ( + + + Folder (optional) + + + + field.onChange(e.target.value || null) + } + value={field.value ?? ""} + /> + + +

+ Use slashes for nested folders (e.g., auth/login) +

+
+ )} + /> {/* Separator */} diff --git a/apps/dashboard/app/(main)/websites/[id]/flags/_components/flags-list.tsx b/apps/dashboard/app/(main)/websites/[id]/flags/_components/flags-list.tsx index 494263900..de0ea2aa3 100644 --- a/apps/dashboard/app/(main)/websites/[id]/flags/_components/flags-list.tsx +++ b/apps/dashboard/app/(main)/websites/[id]/flags/_components/flags-list.tsx @@ -2,9 +2,12 @@ import { ArchiveIcon, + CaretDownIcon, + CaretRightIcon, DotsThreeIcon, FlagIcon, FlaskIcon, + Folder, GaugeIcon, LinkIcon, PencilSimpleIcon, @@ -12,7 +15,7 @@ import { TrashIcon, } from "@phosphor-icons/react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { @@ -43,6 +46,13 @@ interface FlagsListProps { onDelete: (flagId: string) => void; } +interface FolderGroup { + name: string; + path: string; + flags: Flag[]; + children: Map; +} + const TYPE_CONFIG = { boolean: { icon: FlagIcon, label: "Boolean", color: "text-blue-500" }, rollout: { icon: GaugeIcon, label: "Rollout", color: "text-violet-500" }, @@ -359,7 +369,7 @@ function FlagRow({
{ruleCount > 0 && ( - {ruleCount} {ruleCount !== 1 ? "rules" : "rule"} + {ruleCount} {ruleCount === 1 ? "rule" : "rules"} )} {variantCount > 0 && ( @@ -427,15 +437,84 @@ export function FlagsList({ flags, groups, onEdit, onDelete }: FlagsListProps) { return map; }, [flags]); + // Group flags by folder + const folderGroups = useMemo(() => { + const root: FolderGroup = { + name: "Root", + path: "", + flags: [], + children: new Map(), + }; + + for (const flag of flags) { + if (flag.folder) { + const parts = flag.folder.split("/"); + let currentNode = root; + + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + const currentPath = parts.slice(0, i + 1).join("/"); + + if (!currentNode.children.has(part)) { + currentNode.children.set(part, { + name: part, + path: currentPath, + flags: [], + children: new Map(), + }); + } + + const nextNode = currentNode.children.get(part); + if (nextNode) { + currentNode = nextNode; + } + } + + currentNode.flags.push(flag); + } else { + root.flags.push(flag); + } + } + + return root; + }, [flags]); + return (
- {flags.map((flag) => ( - 0 && ( +
+
+ + + Uncategorized ({folderGroups.flags.length}) + +
+ {folderGroups.flags.map((flag) => ( + + ))} +
+ )} + + {/* Folder groups */} + {Array.from(folderGroups.children.values()).map((folder) => ( + @@ -444,6 +523,154 @@ export function FlagsList({ flags, groups, onEdit, onDelete }: FlagsListProps) { ); } +function FolderSection({ + folder, + flagMap, + dependentsMap, + groups, + onEdit, + onDelete, +}: { + folder: FolderGroup; + flagMap: Map; + dependentsMap: Map; + groups: Map; + onEdit: (flag: Flag) => void; + onDelete: (flagId: string) => void; +}) { + const [isExpanded, setIsExpanded] = useState(true); + const totalFlags = countAllFlags(folder); + + return ( +
+ + + {isExpanded && ( +
+ {/* Direct flags in this folder */} + {folder.flags.map((flag) => ( + + ))} + + {/* Nested folders */} + {Array.from(folder.children.values()).map((childFolder) => ( + + ))} +
+ )} +
+ ); +} + +function NestedFolderSection({ + folder, + flagMap, + dependentsMap, + groups, + onEdit, + onDelete, +}: { + folder: FolderGroup; + flagMap: Map; + dependentsMap: Map; + groups: Map; + onEdit: (flag: Flag) => void; + onDelete: (flagId: string) => void; +}) { + const [isExpanded, setIsExpanded] = useState(true); + const totalFlags = countAllFlags(folder); + + return ( +
+ + + {isExpanded && ( +
+ {folder.flags.map((flag) => ( + + ))} + + {Array.from(folder.children.values()).map((childFolder) => ( + + ))} +
+ )} +
+ ); +} + +function countAllFlags(folder: FolderGroup): number { + let count = folder.flags.length; + for (const child of folder.children.values()) { + count += countAllFlags(child); + } + return count; +} + export function FlagsListSkeleton() { return (
diff --git a/apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-selector.tsx b/apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-selector.tsx new file mode 100644 index 000000000..dbf8c072a --- /dev/null +++ b/apps/dashboard/app/(main)/websites/[id]/flags/_components/folder-selector.tsx @@ -0,0 +1,201 @@ +"use client"; + +import { Folder, FolderOpen } from "@phosphor-icons/react/dist/ssr"; +import { CheckIcon, PlusIcon } from "lucide-react"; +import { useMemo, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { cn } from "@/lib/utils"; + +interface FolderOption { + value: string; + label: string; + disabled?: boolean; +} + +interface FolderSelectorProps { + value?: string | null; + onChange: (folder: string | null) => void; + availableFolders?: string[]; + placeholder?: string; + className?: string; + disabled?: boolean; + onCreateFolder?: (folder: string) => void; +} + +export function FolderSelector({ + value, + onChange, + availableFolders = [], + placeholder = "Select folder...", + className, + disabled, + onCreateFolder, +}: FolderSelectorProps) { + const [open, setOpen] = useState(false); + const [searchValue, setSearchValue] = useState(""); + + // Extract unique folders and build hierarchy + const folderOptions = useMemo(() => { + const folders = new Set(); + + // Add existing folders + for (const folder of availableFolders) { + if (folder) { + folders.add(folder); + } + } + + // Add current value if not in list + if (value && !folders.has(value)) { + folders.add(value); + } + + // Convert to options with hierarchy display + const options: FolderOption[] = Array.from(folders) + .sort() + .map((folder) => ({ + value: folder, + label: folder, + })); + + return options; + }, [availableFolders, value]); + + // Filter folders based on search + const filteredOptions = useMemo(() => { + if (!searchValue.trim()) { + return folderOptions; + } + return folderOptions.filter((option) => + option.label.toLowerCase().includes(searchValue.toLowerCase()) + ); + }, [folderOptions, searchValue]); + + // Get parent folders for display + const getFolderDisplay = (folder: string) => { + const parts = folder.split("/"); + if (parts.length === 1) { + return folder; + } + return parts.join(" / "); + }; + + const handleSelect = (folderValue: string) => { + onChange(folderValue === value ? null : folderValue); + setOpen(false); + }; + + const handleCreateFolder = () => { + if (searchValue.trim() && onCreateFolder) { + onCreateFolder(searchValue.trim()); + setSearchValue(""); + } + }; + + return ( + + + + + + + + + + {searchValue.trim() && onCreateFolder ? ( +
+ + No folder found. Create one? + + +
+ ) : ( + "No folders found." + )} +
+ + {/* No Folder Option */} + onChange(null)} + value="__no_folder__" + > + + No folder (root) + + + {/* Existing Folders */} + {filteredOptions.map((option) => ( + handleSelect(option.value)} + value={option.value} + > + + + + {getFolderDisplay(option.value)} + + + ))} + +
+
+
+
+ ); +} diff --git a/apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts b/apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts index 8410ed0b8..0fcbd27cd 100644 --- a/apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts +++ b/apps/dashboard/app/(main)/websites/[id]/flags/_components/types.ts @@ -27,6 +27,7 @@ export interface Flag { organizationId?: string | null; userId?: string | null; createdBy: string; + folder?: string | null; createdAt: Date; updatedAt: Date; deletedAt?: Date | null; diff --git a/biome.jsonc b/biome.jsonc index 95c67a0d8..69df07a54 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -58,7 +58,6 @@ }, "nursery": { "noShadow": "off", - "useMaxParams": "off", "noLeakedRender": "off" }, "a11y": { diff --git a/bun.lock b/bun.lock index 8616b471e..1d26c6bc8 100644 --- a/bun.lock +++ b/bun.lock @@ -114,7 +114,7 @@ "name": "@databuddy/dashboard", "version": "0.1.0", "dependencies": { - "@ai-sdk/react": "^3.0.0", + "@ai-sdk/react": "^3.0.118", "@cossistant/next": "^0.0.29", "@cossistant/react": "^0.0.29", "@databuddy/api-keys": "workspace:*", @@ -128,8 +128,8 @@ "@hello-pangea/dnd": "^18.0.1", "@hookform/resolvers": "^5.2.2", "@json-render/react": "^0.2.0", - "@orpc/client": "^1.13.0", - "@orpc/tanstack-query": "^1.13.0", + "@orpc/client": "^1.13.9", + "@orpc/tanstack-query": "^1.13.9", "@phosphor-icons/react": "^2.1.10", "@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-collapsible": "^1.1.12", @@ -144,79 +144,79 @@ "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-use-controllable-state": "^1.2.2", - "@tanstack/react-pacer": "^0.19.2", - "@tanstack/react-query": "^5.90.12", + "@tanstack/react-pacer": "^0.19.4", + "@tanstack/react-query": "^5.91.2", "@tanstack/react-table": "^8.21.3", "@types/d3-scale": "^4.0.9", "@types/geojson": "^7946.0.16", "@types/leaflet": "^1.9.21", "@types/react-grid-layout": "^2.1.0", - "@xyflow/react": "^12.10.0", - "ai": "^6.0.0", - "autumn-js": "^0.1.63", + "@xyflow/react": "^12.10.1", + "ai": "^6.0.116", + "autumn-js": "^0.1.85", "babel-plugin-react-compiler": "^19.1.0-rc.1-rc-af1b7da-20250421", - "better-auth": "^1.4.9", + "better-auth": "^1.5.5", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "d3-scale": "^4.0.2", - "dayjs": "^1.11.19", + "dayjs": "^1.11.20", "embla-carousel-react": "^8.6.0", "flag-icons": "^7.5.0", - "framer-motion": "^12.23.26", + "framer-motion": "^12.38.0", "idb": "^8.0.3", "input-otp": "^1.4.2", - "jotai": "^2.16.0", + "jotai": "^2.18.1", "leaflet": "^1.9.4", "lucide-react": "^0.562.0", - "maplibre-gl": "^5.15.0", - "motion": "^12.23.26", - "nanoid": "^5.1.6", - "next": "^16.1.1", + "maplibre-gl": "^5.20.2", + "motion": "^12.38.0", + "nanoid": "^5.1.7", + "next": "^16.2.0", "next-themes": "^0.4.6", - "nuqs": "^2.8.6", + "nuqs": "^2.8.9", "ogl": "^1.0.11", - "pg": "^8.16.3", + "pg": "^8.20.0", "qrcode.react": "^4.2.0", "radix-ui": "latest", "react": "catalog:", - "react-day-picker": "^9.13.0", + "react-day-picker": "^9.14.0", "react-dom": "catalog:", "react-grid-layout": "^2.2.2", - "react-hook-form": "^7.69.0", - "react-hotkeys-hook": "^5.2.1", + "react-hook-form": "^7.71.2", + "react-hotkeys-hook": "^5.2.4", "react-image-crop": "^11.0.10", "react-leaflet": "^5.0.0", "react-qrcode-logo": "^4.0.0", "react-resizable-panels": "^3.0.6", "react-textarea-autosize": "^8.5.9", "recharts": "^2.15.4", - "shiki": "^3.20.0", - "simple-icons": "^16.2.0", + "shiki": "^3.23.0", + "simple-icons": "^16.12.0", "sonner": "^2.0.7", - "streamdown": "^2.1.0", - "tailwind-merge": "^3.4.0", + "streamdown": "^2.5.0", + "tailwind-merge": "^3.5.0", "tokenlens": "^1.3.1", "tw-animate-css": "^1.4.0", - "use-stick-to-bottom": "^1.1.1", + "use-stick-to-bottom": "^1.1.3", "vaul": "^1.1.2", "zod": "catalog:", }, "devDependencies": { "@biomejs/biome": "catalog:", - "@orpc/server": "^1.13.0", - "@tailwindcss/postcss": "^4.1.18", - "@tanstack/react-query-devtools": "^5.91.2", + "@orpc/server": "^1.13.9", + "@tailwindcss/postcss": "^4.2.2", + "@tanstack/react-query-devtools": "^5.91.3", "@types/d3-geo": "^3.1.0", - "@types/node": "^22.19.3", - "@types/pg": "^8.16.0", + "@types/node": "^22.19.15", + "@types/pg": "^8.18.0", "@types/react": "catalog:", "@types/react-dom": "catalog:", "@types/react-simple-maps": "^3.0.6", "@types/topojson-client": "^3.1.5", "husky": "^9.1.7", - "lint-staged": "^16.2.7", - "tailwindcss": "^4.1.18", + "lint-staged": "^16.4.0", + "tailwindcss": "^4.2.2", "typescript": "^5.9.3", "ultracite": "catalog:", }, @@ -767,7 +767,7 @@ "@balena/dockerignore": ["@balena/dockerignore@1.0.2", "", {}, "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="], - "@better-auth/core": ["@better-auth/core@1.4.10", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.1.12" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.7", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-AThrfb6CpG80wqkanfrbN2/fGOYzhGladHFf3JhaWt/3/Vtf4h084T6PJLrDE7M/vCCGYvDI1DkvP3P1OB2HAg=="], + "@better-auth/core": ["@better-auth/core@1.5.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-1oR/2jAp821Dcf67kQYHUoyNcdc1TcShfw4QMK0YTVntuRES5mUOyvEJql5T6eIuLfaqaN4LOF78l0FtF66HXA=="], "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.5.5", "", { "peerDependencies": { "@better-auth/core": "1.5.5", "@better-auth/utils": "^0.3.0", "drizzle-orm": ">=0.41.0" }, "optionalPeers": ["drizzle-orm"] }, "sha512-HAi9xAP40oDt48QZeYBFTcmg3vt1Jik90GwoRIfangd7VGbxesIIDBJSnvwMbZ52GBIc6+V4FRw9lasNiNrPfw=="], @@ -781,9 +781,9 @@ "@better-auth/sso": ["@better-auth/sso@1.4.10", "", { "dependencies": { "@better-fetch/fetch": "1.1.21", "fast-xml-parser": "^5.2.5", "jose": "^6.1.0", "samlify": "^2.10.1", "zod": "^4.1.12" }, "peerDependencies": { "better-auth": "1.4.10" } }, "sha512-td8Mg32JHpyFRIwJ6sfqZ0NDa9Easf+sXw5wnWLLgmnd7/osg4xTKTFsMLEvr5j4n/1mzSFVU/RBthOV2lCD+A=="], - "@better-auth/telemetry": ["@better-auth/telemetry@1.4.10", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.10" } }, "sha512-Dq4XJX6EKsUu0h3jpRagX739p/VMOTcnJYWRrLtDYkqtZFg+sFiFsSWVcfapZoWpRSUGYX9iKwl6nDHn6Ju2oQ=="], + "@better-auth/telemetry": ["@better-auth/telemetry@1.5.5", "", { "dependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.5.5" } }, "sha512-1+lklxArn4IMHuU503RcPdXrSG2tlXt4jnGG3omolmspQ7tktg/Y9XO/yAkYDurtvMn1xJ8X1Ov01Ji/r5s9BQ=="], - "@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="], + "@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], @@ -2027,7 +2027,7 @@ "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="], - "better-auth": ["better-auth@1.4.10", "", { "dependencies": { "@better-auth/core": "1.4.10", "@better-auth/telemetry": "1.4.10", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.7", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.12" }, "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", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "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", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-0kqwEBJLe8eyFzbUspRG/htOriCf9uMLlnpe34dlIJGdmDfPuQISd4shShvUrvIVhPxsY1dSTXdXPLpqISYOYg=="], + "better-auth": ["better-auth@1.5.5", "", { "dependencies": { "@better-auth/core": "1.5.5", "@better-auth/drizzle-adapter": "1.5.5", "@better-auth/kysely-adapter": "1.5.5", "@better-auth/memory-adapter": "1.5.5", "@better-auth/mongo-adapter": "1.5.5", "@better-auth/prisma-adapter": "1.5.5", "@better-auth/telemetry": "1.5.5", "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.2", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.11", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "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.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "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", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-GpVPaV1eqr3mOovKfghJXXk6QvlcVeFbS3z+n+FPDid5rK/2PchnDtiaVCzWyXA9jH2KkirOfl+JhAUvnja0Eg=="], "better-auth-harmony": ["better-auth-harmony@1.2.5", "", { "dependencies": { "libphonenumber-js": "^1.12.8", "mailchecker": "^6.0.17", "validator": "^13.15.15" }, "peerDependencies": { "better-auth": "^1.0.3" } }, "sha512-4YaAK5vrLnB6heImYJB8Pf524BPFrOYmUy1IFTHk6btGDCbgh3xT/hBCM6Ougwv/drURxtfZlB/FPktIjKLMtg=="], @@ -3075,7 +3075,7 @@ "nanoid": ["nanoid@5.1.7", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ=="], - "nanostores": ["nanostores@1.1.0", "", {}, "sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA=="], + "nanostores": ["nanostores@1.2.0", "", {}, "sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg=="], "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], @@ -3999,17 +3999,11 @@ "@better-auth/core/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@better-auth/core/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], - - "@better-auth/drizzle-adapter/@better-auth/core": ["@better-auth/core@1.5.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-1oR/2jAp821Dcf67kQYHUoyNcdc1TcShfw4QMK0YTVntuRES5mUOyvEJql5T6eIuLfaqaN4LOF78l0FtF66HXA=="], - - "@better-auth/kysely-adapter/@better-auth/core": ["@better-auth/core@1.5.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-1oR/2jAp821Dcf67kQYHUoyNcdc1TcShfw4QMK0YTVntuRES5mUOyvEJql5T6eIuLfaqaN4LOF78l0FtF66HXA=="], + "@better-auth/core/better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="], - "@better-auth/memory-adapter/@better-auth/core": ["@better-auth/core@1.5.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-1oR/2jAp821Dcf67kQYHUoyNcdc1TcShfw4QMK0YTVntuRES5mUOyvEJql5T6eIuLfaqaN4LOF78l0FtF66HXA=="], + "@better-auth/core/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - "@better-auth/mongo-adapter/@better-auth/core": ["@better-auth/core@1.5.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-1oR/2jAp821Dcf67kQYHUoyNcdc1TcShfw4QMK0YTVntuRES5mUOyvEJql5T6eIuLfaqaN4LOF78l0FtF66HXA=="], - - "@better-auth/prisma-adapter/@better-auth/core": ["@better-auth/core@1.5.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-1oR/2jAp821Dcf67kQYHUoyNcdc1TcShfw4QMK0YTVntuRES5mUOyvEJql5T6eIuLfaqaN4LOF78l0FtF66HXA=="], + "@better-auth/sso/better-auth": ["better-auth@1.4.10", "", { "dependencies": { "@better-auth/core": "1.4.10", "@better-auth/telemetry": "1.4.10", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.7", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.12" }, "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", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "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", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-0kqwEBJLe8eyFzbUspRG/htOriCf9uMLlnpe34dlIJGdmDfPuQISd4shShvUrvIVhPxsY1dSTXdXPLpqISYOYg=="], "@better-auth/sso/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], @@ -4043,8 +4037,6 @@ "@databuddy/dashboard/autumn-js": ["autumn-js@0.1.85", "", { "dependencies": { "query-string": "^9.2.2", "rou3": "^0.6.1", "swr": "^2.3.3", "zod": "^4.0.0" }, "peerDependencies": { "better-auth": "^1.3.17", "better-call": "^1.0.12", "convex": "^1.25.4" }, "optionalPeers": ["better-auth", "better-call", "convex"] }, "sha512-PDud/t8z5bDJcD7ptyHzTaoJ0A8zkxvQ4TYcJ48RtgKDdOkVY36D1T6udVLwLDnWw4J5KXwJgEuGxHdd+cuABw=="], - "@databuddy/dashboard/better-auth": ["better-auth@1.5.5", "", { "dependencies": { "@better-auth/core": "1.5.5", "@better-auth/drizzle-adapter": "1.5.5", "@better-auth/kysely-adapter": "1.5.5", "@better-auth/memory-adapter": "1.5.5", "@better-auth/mongo-adapter": "1.5.5", "@better-auth/prisma-adapter": "1.5.5", "@better-auth/telemetry": "1.5.5", "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.2", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.11", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "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.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "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", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-GpVPaV1eqr3mOovKfghJXXk6QvlcVeFbS3z+n+FPDid5rK/2PchnDtiaVCzWyXA9jH2KkirOfl+JhAUvnja0Eg=="], - "@databuddy/dashboard/dayjs": ["dayjs@1.11.20", "", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="], "@databuddy/dashboard/tokenlens": ["tokenlens@1.3.1", "", { "dependencies": { "@tokenlens/core": "1.3.0", "@tokenlens/fetch": "1.3.0", "@tokenlens/helpers": "1.3.1", "@tokenlens/models": "1.3.0" } }, "sha512-7oxmsS5PNCX3z+b+z07hL5vCzlgHKkCGrEQjQmWl5l+v5cUrtL7S1cuST4XThaL1XyjbTX8J5hfP0cjDJRkaLA=="], @@ -4563,10 +4555,16 @@ "babel-plugin-react-compiler/@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], - "better-auth/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], + "better-auth/better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="], + + "better-auth/kysely": ["kysely@0.28.13", "", {}, "sha512-jCkYDvlfzOyHaVsrvR4vnNZxG30oNv2jbbFBjTQAUG8n0h07HW0sZJHk4KAQIRyu9ay+Rg+L8qGa3lwt8Gve9w=="], + + "better-auth/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "better-auth-harmony/libphonenumber-js": ["libphonenumber-js@1.12.35", "", {}, "sha512-T/Cz6iLcsZdb5jDncDcUNhSAJ0VlSC9TnsqtBNdpkaAmy24/R1RhErtNWVWBrcUZKs9hSgaVsBkc7HxYnazIfw=="], + "better-call/@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="], + "better-call/rou3": ["rou3@0.7.11", "", {}, "sha512-ELguG3ENDw5NKNmWHO3OGEjcgdxkCNvnMR22gKHEgRXuwiriap5RIYdummOaOiqUNcC5yU5txGCHWNm7KlHuAA=="], "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], @@ -4843,45 +4841,15 @@ "@babel/helper-skip-transparent-expression-wrappers/@babel/traverse/@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], - "@better-auth/drizzle-adapter/@better-auth/core/@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], - - "@better-auth/drizzle-adapter/@better-auth/core/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@better-auth/drizzle-adapter/@better-auth/core/better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="], - - "@better-auth/drizzle-adapter/@better-auth/core/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - - "@better-auth/kysely-adapter/@better-auth/core/@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], - - "@better-auth/kysely-adapter/@better-auth/core/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@better-auth/kysely-adapter/@better-auth/core/better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="], + "@better-auth/core/better-call/set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="], - "@better-auth/kysely-adapter/@better-auth/core/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "@better-auth/sso/better-auth/@better-auth/core": ["@better-auth/core@1.4.10", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.1.12" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.7", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-AThrfb6CpG80wqkanfrbN2/fGOYzhGladHFf3JhaWt/3/Vtf4h084T6PJLrDE7M/vCCGYvDI1DkvP3P1OB2HAg=="], - "@better-auth/memory-adapter/@better-auth/core/@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], + "@better-auth/sso/better-auth/@better-auth/telemetry": ["@better-auth/telemetry@1.4.10", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.10" } }, "sha512-Dq4XJX6EKsUu0h3jpRagX739p/VMOTcnJYWRrLtDYkqtZFg+sFiFsSWVcfapZoWpRSUGYX9iKwl6nDHn6Ju2oQ=="], - "@better-auth/memory-adapter/@better-auth/core/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@better-auth/sso/better-auth/@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="], - "@better-auth/memory-adapter/@better-auth/core/better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="], - - "@better-auth/memory-adapter/@better-auth/core/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - - "@better-auth/mongo-adapter/@better-auth/core/@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], - - "@better-auth/mongo-adapter/@better-auth/core/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@better-auth/mongo-adapter/@better-auth/core/better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="], - - "@better-auth/mongo-adapter/@better-auth/core/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - - "@better-auth/prisma-adapter/@better-auth/core/@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], - - "@better-auth/prisma-adapter/@better-auth/core/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@better-auth/prisma-adapter/@better-auth/core/better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="], - - "@better-auth/prisma-adapter/@better-auth/core/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "@better-auth/sso/better-auth/nanostores": ["nanostores@1.1.0", "", {}, "sha512-yJBmDJr18xy47dbNVlHcgdPrulSn1nhSE6Ns9vTG+Nx9VPT6iV1MD6aQFp/t52zpf82FhLLTXAXr30NuCnxvwA=="], "@databuddy/ai/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], @@ -5019,20 +4987,6 @@ "@databuddy/dashboard/autumn-js/zod": ["zod@4.2.1", "", {}, "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw=="], - "@databuddy/dashboard/better-auth/@better-auth/core": ["@better-auth/core@1.5.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "better-call": "1.3.2", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-1oR/2jAp821Dcf67kQYHUoyNcdc1TcShfw4QMK0YTVntuRES5mUOyvEJql5T6eIuLfaqaN4LOF78l0FtF66HXA=="], - - "@databuddy/dashboard/better-auth/@better-auth/telemetry": ["@better-auth/telemetry@1.5.5", "", { "dependencies": { "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.5.5" } }, "sha512-1+lklxArn4IMHuU503RcPdXrSG2tlXt4jnGG3omolmspQ7tktg/Y9XO/yAkYDurtvMn1xJ8X1Ov01Ji/r5s9BQ=="], - - "@databuddy/dashboard/better-auth/@better-auth/utils": ["@better-auth/utils@0.3.1", "", {}, "sha512-+CGp4UmZSUrHHnpHhLPYu6cV+wSUSvVbZbNykxhUDocpVNTo9uFFxw/NqJlh1iC4wQ9HKKWGCKuZ5wUgS0v6Kg=="], - - "@databuddy/dashboard/better-auth/better-call": ["better-call@1.3.2", "", { "dependencies": { "@better-auth/utils": "^0.3.1", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-4cZIfrerDsNTn3cm+MhLbUePN0gdwkhSXEuG7r/zuQ8c/H7iU0/jSK5TD3FW7U0MgKHce/8jGpPYNO4Ve+4NBw=="], - - "@databuddy/dashboard/better-auth/kysely": ["kysely@0.28.13", "", {}, "sha512-jCkYDvlfzOyHaVsrvR4vnNZxG30oNv2jbbFBjTQAUG8n0h07HW0sZJHk4KAQIRyu9ay+Rg+L8qGa3lwt8Gve9w=="], - - "@databuddy/dashboard/better-auth/nanostores": ["nanostores@1.2.0", "", {}, "sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg=="], - - "@databuddy/dashboard/better-auth/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - "@databuddy/dashboard/tokenlens/@tokenlens/core": ["@tokenlens/core@1.3.0", "", {}, "sha512-d8YNHNC+q10bVpi95fELJwJyPVf1HfvBEI18eFQxRSZTdByXrP+f/ZtlhSzkx0Jl0aEmYVeBA5tPeeYRioLViQ=="], "@databuddy/dashboard/tokenlens/@tokenlens/fetch": ["@tokenlens/fetch@1.3.0", "", { "dependencies": { "@tokenlens/core": "1.3.0" } }, "sha512-RONDRmETYly9xO8XMKblmrZjKSwCva4s5ebJwQNfNlChZoA5kplPoCgnWceHnn1J1iRjLVlrCNB43ichfmGBKQ=="], @@ -5525,6 +5479,8 @@ "@xyflow/system/@types/d3-interpolate/@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + "better-auth/better-call/set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="], + "bl/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -5823,15 +5779,7 @@ "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@better-auth/drizzle-adapter/@better-auth/core/better-call/set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="], - - "@better-auth/kysely-adapter/@better-auth/core/better-call/set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="], - - "@better-auth/memory-adapter/@better-auth/core/better-call/set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="], - - "@better-auth/mongo-adapter/@better-auth/core/better-call/set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="], - - "@better-auth/prisma-adapter/@better-auth/core/better-call/set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="], + "@better-auth/sso/better-auth/@better-auth/core/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@databuddy/api/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.208.0", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.208.0", "@opentelemetry/otlp-transformer": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-fGvAg3zb8fC0oJAzfz7PQppADI2HYB7TSt/XoCaBJFi1mSquNUjtHXEoviMgObLAa1NRIgOC1lsV1OUKi+9+lQ=="], @@ -5855,10 +5803,6 @@ "@databuddy/dashboard/ai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@databuddy/dashboard/better-auth/@better-auth/core/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@databuddy/dashboard/better-auth/better-call/set-cookie-parser": ["set-cookie-parser@3.0.1", "", {}, "sha512-n7Z7dXZhJbwuAHhNzkTti6Aw9QDDjZtm3JTpTGATIdNzdQz5GuFs22w90BcvF4INfnrL5xrX3oGsuqO5Dx3A1Q=="], - "@databuddy/dashboard/ultracite/trpc-cli/@trpc/server": ["@trpc/server@11.7.2", "", { "peerDependencies": { "typescript": ">=5.7.2" } }, "sha512-AgB26PXY69sckherIhCacKLY49rxE2XP5h38vr/KMZTbLCL1p8IuIoKPjALTcugC2kbyQ7Lbqo2JDVfRSmPmfQ=="], "@databuddy/dashboard/ultracite/trpc-cli/commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], diff --git a/packages/db/src/drizzle/schema.ts b/packages/db/src/drizzle/schema.ts index 214ed8384..7006cc349 100644 --- a/packages/db/src/drizzle/schema.ts +++ b/packages/db/src/drizzle/schema.ts @@ -669,6 +669,7 @@ export const flags = pgTable( dependencies: text("dependencies").array(), targetGroupIds: text("target_group_ids").array(), environment: text("environment"), + folder: text("folder"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().notNull(), deletedAt: timestamp("deleted_at"), @@ -687,6 +688,10 @@ export const flags = pgTable( "btree", table.createdBy.asc().nullsLast().op("text_ops") ), + index("idx_flags_folder").using( + "btree", + table.folder.asc().nullsLast().op("text_ops") + ), foreignKey({ columns: [table.websiteId], foreignColumns: [websites.id], @@ -1037,10 +1042,7 @@ export const feedback = pgTable( "btree", table.organizationId.asc().nullsLast().op("text_ops") ), - index("feedback_status_idx").using( - "btree", - table.status.asc().nullsLast() - ), + index("feedback_status_idx").using("btree", table.status.asc().nullsLast()), foreignKey({ columns: [table.userId], foreignColumns: [user.id], diff --git a/packages/rpc/src/routers/flags.ts b/packages/rpc/src/routers/flags.ts index 83cdef619..3a669eb2a 100644 --- a/packages/rpc/src/routers/flags.ts +++ b/packages/rpc/src/routers/flags.ts @@ -29,11 +29,11 @@ import { z } from "zod"; import { rpcError } from "../errors"; import type { Context } from "../orpc"; import { protectedProcedure, publicProcedure } from "../orpc"; +import { isFullyAuthorized, withWorkspace } from "../procedures/with-workspace"; import { - isFullyAuthorized, - withWorkspace, -} from "../procedures/with-workspace"; -import { requireFeatureWithLimit, requireUsageWithinLimit } from "../types/billing"; + requireFeatureWithLimit, + requireUsageWithinLimit, +} from "../types/billing"; import { getCacheAuthContext } from "../utils/cache-keys"; const flagsCache = createDrizzleCache({ redis, namespace: "flags" }); @@ -101,6 +101,7 @@ const createFlagSchema = z organizationId: z.string().optional(), payload: z.any().optional(), persistAcrossAuth: z.boolean().optional(), + folder: z.string().max(200, "Folder path too long").nullable().optional(), ...flagFormSchema.shape, }) .refine((data) => data.websiteId || data.organizationId, { @@ -125,6 +126,7 @@ const updateFlagSchema = z dependencies: z.array(z.string()).optional(), environment: z.string().optional(), targetGroupIds: z.array(z.string()).optional(), + folder: z.string().max(200, "Folder path too long").nullable().optional(), }) .superRefine((data, ctx) => { if (data.type === "multivariant" && data.variants) { @@ -225,7 +227,7 @@ function sanitizeFlagForDemo(flag: T): T { ...flag, rules: Array.isArray(flag.rules) && flag.rules.length > 0 ? [] : flag.rules, targetGroups: flag.targetGroups?.map( - (group: { rules?: unknown;[key: string]: unknown }) => ({ + (group: { rules?: unknown; [key: string]: unknown }) => ({ ...group, rules: Array.isArray(group.rules) && group.rules.length > 0 @@ -479,14 +481,14 @@ export const flagsRouter = { const workspace = wsId ? await withWorkspace(context, { - websiteId: wsId, - permissions: ["update"], - }) + websiteId: wsId, + permissions: ["update"], + }) : await withWorkspace(context, { - organizationId: orgId, - resource: "website", - permissions: ["create"], - }); + organizationId: orgId, + resource: "website", + permissions: ["create"], + }); const createdBy = await workspace.getCreatedBy(); @@ -572,6 +574,10 @@ export const flagsRouter = { variants: input.variants, dependencies: input.dependencies, environment: input.environment, + folder: + input.folder === undefined + ? existingFlag[0].folder + : input.folder, deletedAt: null, updatedAt: new Date(), }) @@ -629,6 +635,7 @@ export const flagsRouter = { websiteId: input.websiteId || null, organizationId: input.organizationId || null, environment: input.environment || existingFlag?.[0]?.environment, + folder: input.folder || null, userId: null, createdBy, }) @@ -696,23 +703,22 @@ export const flagsRouter = { const flag = existingFlag[0]; - let workspace; - if (flag.websiteId) { - workspace = await withWorkspace(context, { - websiteId: flag.websiteId, - permissions: ["update"], - }); - } else if (flag.organizationId) { - workspace = await withWorkspace(context, { - organizationId: flag.organizationId, - resource: "website", - permissions: ["create"], - }); - } else { - throw rpcError.forbidden( - "Flags must be scoped to a website or organization" - ); - } + const workspace = flag.websiteId + ? await withWorkspace(context, { + websiteId: flag.websiteId, + permissions: ["update"], + }) + : flag.organizationId + ? await withWorkspace(context, { + organizationId: flag.organizationId, + resource: "website", + permissions: ["create"], + }) + : (() => { + throw rpcError.forbidden( + "Flags must be scoped to a website or organization" + ); + })(); const isUnarchiving = flag.status === "archived" && diff --git a/packages/shared/src/flags/index.ts b/packages/shared/src/flags/index.ts index 59183a816..230581c8e 100644 --- a/packages/shared/src/flags/index.ts +++ b/packages/shared/src/flags/index.ts @@ -62,6 +62,7 @@ export const flagFormSchema = z .optional(), environment: z.string().nullable().optional(), targetGroupIds: z.array(z.string()).optional(), + folder: z.string().max(200, "Folder path too long").nullable().optional(), }) .superRefine((data, ctx) => { if (data.type === "multivariant" && data.variants) { @@ -116,38 +117,7 @@ export const flagScheduleSchema = z if (!data.isEnabled) { return; } - if (data.type !== "update_rollout") { - if (data.rolloutSteps && data?.rolloutSteps?.length > 0) { - ctx.addIssue({ - code: "custom", - path: ["rolloutSteps"], - message: "Rollout steps allowed only for update_rollout type", - }); - } - if (!data.scheduledAt) { - ctx.addIssue({ - code: "custom", - path: ["scheduledAt"], - message: "Date time is required for enable/disable schedule types", - }); - } - const scheduledDate = new Date(data.scheduledAt ?? ""); - if (Number.isNaN(scheduledDate.getTime())) { - ctx.addIssue({ - code: "custom", - path: ["scheduledAt"], - message: "Invalid schedule date", - }); - } - - if (Date.now() > new Date(data.scheduledAt ?? "").getTime()) { - ctx.addIssue({ - code: "custom", - path: ["scheduledAt"], - message: "Scheduled time must be in the future", - }); - } - } else { + if (data.type === "update_rollout") { if (data.scheduledAt) { ctx.addIssue({ code: "custom", @@ -191,6 +161,37 @@ export const flagScheduleSchema = z }); } } + } else { + if (data.rolloutSteps && data?.rolloutSteps?.length > 0) { + ctx.addIssue({ + code: "custom", + path: ["rolloutSteps"], + message: "Rollout steps allowed only for update_rollout type", + }); + } + if (!data.scheduledAt) { + ctx.addIssue({ + code: "custom", + path: ["scheduledAt"], + message: "Date time is required for enable/disable schedule types", + }); + } + const scheduledDate = new Date(data.scheduledAt ?? ""); + if (Number.isNaN(scheduledDate.getTime())) { + ctx.addIssue({ + code: "custom", + path: ["scheduledAt"], + message: "Invalid schedule date", + }); + } + + if (Date.now() > new Date(data.scheduledAt ?? "").getTime()) { + ctx.addIssue({ + code: "custom", + path: ["scheduledAt"], + message: "Scheduled time must be in the future", + }); + } } }); export type FlagSchedule = z.infer;