this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add Clerk authentication to API and web app (#3)

authored by

James Blair and committed by
GitHub
f7f073b8 bcb084d8

+489 -125
+2 -1
apps/api/drizzle.config.ts
··· 1 1 import { defineConfig } from "drizzle-kit"; 2 + import { env } from "./src/env"; 2 3 3 4 export default defineConfig({ 4 5 out: "./drizzle", 5 6 dialect: "sqlite", 6 7 schema: "./src/lib/db/schema.ts", 7 8 dbCredentials: { 8 - url: process.env.DB_FILE_NAME ?? "sqlite.db", 9 + url: env.DB_FILE_NAME, 9 10 }, 10 11 });
+2
apps/api/package.json
··· 15 15 }, 16 16 "dependencies": { 17 17 "@ai-sdk/openai": "^3.0.53", 18 + "@clerk/backend": "^3.4.1", 18 19 "@hono/zod-validator": "^0.7.6", 20 + "@t3-oss/env-core": "^0.13.11", 19 21 "ai": "^6.0.168", 20 22 "drizzle-orm": "^0.45.2", 21 23 "hono": "^4.12.15",
+17
apps/api/src/env.ts
··· 1 + import { createEnv } from "@t3-oss/env-core"; 2 + import { z } from "zod"; 3 + 4 + export const env = createEnv({ 5 + server: { 6 + DB_FILE_NAME: z.string().min(1).default("sqlite.db"), 7 + OPENAI_API_KEY: z.string().min(1), 8 + CLERK_PUBLISHABLE_KEY: z.string().min(1), 9 + CLERK_SECRET_KEY: z.string().min(1), 10 + CLERK_AUTHORIZED_PARTIES: z 11 + .string() 12 + .min(1) 13 + .default("http://localhost:3000"), 14 + }, 15 + runtimeEnv: process.env, 16 + emptyStringAsUndefined: true, 17 + });
+2 -1
apps/api/src/lib/db/index.ts
··· 1 1 import { Database } from "bun:sqlite"; 2 2 import { drizzle } from "drizzle-orm/bun-sqlite"; 3 + import { env } from "../../env"; 3 4 import * as schema from "./schema"; 4 5 5 - const sqlite = new Database(process.env.DB_FILE_NAME ?? "sqlite.db"); 6 + const sqlite = new Database(env.DB_FILE_NAME); 6 7 7 8 const db = drizzle({ client: sqlite, schema }); 8 9
+36 -21
apps/api/src/lib/db/schema.ts
··· 1 1 import { relations } from "drizzle-orm"; 2 - import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; 2 + import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; 3 3 import { characterIds } from "../characters"; 4 4 import { modelIds } from "../models"; 5 5 6 - export const chats = sqliteTable("chats", { 7 - id: text("id").primaryKey(), 8 - title: text("title").notNull(), 9 - characterId: text("character_id", { enum: characterIds }).notNull(), 10 - modelId: text("model_id", { enum: modelIds }).notNull(), 11 - createdAt: integer("created_at", { mode: "timestamp_ms" }) 12 - .notNull() 13 - .$defaultFn(() => new Date()), 14 - }); 6 + export const chats = sqliteTable( 7 + "chats", 8 + { 9 + id: text("id").primaryKey(), 10 + userId: text("user_id"), 11 + title: text("title").notNull(), 12 + characterId: text("character_id", { enum: characterIds }).notNull(), 13 + modelId: text("model_id", { enum: modelIds }).notNull(), 14 + createdAt: integer("created_at", { mode: "timestamp_ms" }) 15 + .notNull() 16 + .$defaultFn(() => new Date()), 17 + }, 18 + (table) => ({ 19 + userIdIdx: index("chats_user_id_idx").on(table.userId), 20 + }), 21 + ); 15 22 16 - export const messages = sqliteTable("messages", { 17 - id: text("id").primaryKey(), 18 - chatId: text("chat_id") 19 - .notNull() 20 - .references(() => chats.id, { onDelete: "cascade" }), 21 - role: text("role", { enum: ["assistant", "user"] }).notNull(), 22 - text: text("text").notNull(), 23 - createdAt: integer("created_at", { mode: "timestamp_ms" }) 24 - .notNull() 25 - .$defaultFn(() => new Date()), 26 - }); 23 + export const messages = sqliteTable( 24 + "messages", 25 + { 26 + id: text("id").primaryKey(), 27 + chatId: text("chat_id") 28 + .notNull() 29 + .references(() => chats.id, { onDelete: "cascade" }), 30 + userId: text("user_id"), 31 + role: text("role", { enum: ["assistant", "user"] }).notNull(), 32 + text: text("text").notNull(), 33 + createdAt: integer("created_at", { mode: "timestamp_ms" }) 34 + .notNull() 35 + .$defaultFn(() => new Date()), 36 + }, 37 + (table) => ({ 38 + chatIdIdx: index("messages_chat_id_idx").on(table.chatId), 39 + userIdIdx: index("messages_user_id_idx").on(table.userId), 40 + }), 41 + ); 27 42 28 43 export const chatsRelations = relations(chats, ({ many }) => ({ 29 44 messages: many(messages),
+32
apps/api/src/middlewares/auth.ts
··· 1 + import { createClerkClient } from "@clerk/backend"; 2 + import type { MiddlewareHandler } from "hono"; 3 + import { env } from "../env"; 4 + 5 + type AuthVariables = { 6 + userId: string; 7 + }; 8 + 9 + const clerk = createClerkClient({ 10 + publishableKey: env.CLERK_PUBLISHABLE_KEY, 11 + secretKey: env.CLERK_SECRET_KEY, 12 + }); 13 + 14 + const authorizedParties = env.CLERK_AUTHORIZED_PARTIES.split(",") 15 + .map((party) => party.trim()) 16 + .filter(Boolean); 17 + 18 + export const requireAuth: MiddlewareHandler<{ 19 + Variables: AuthVariables; 20 + }> = async (c, next) => { 21 + const requestState = await clerk.authenticateRequest(c.req.raw, { 22 + authorizedParties, 23 + }); 24 + const auth = requestState.toAuth(); 25 + 26 + if (!auth?.userId) { 27 + return c.json({ message: "Unauthorized" }, 401); 28 + } 29 + 30 + c.set("userId", auth.userId); 31 + await next(); 32 + };
+41 -16
apps/api/src/routes/chats.ts
··· 1 1 import { zValidator } from "@hono/zod-validator"; 2 - import { asc, desc, eq } from "drizzle-orm"; 2 + import { and, asc, desc, eq } from "drizzle-orm"; 3 3 import { Hono } from "hono"; 4 4 import { streamChat } from "../lib/ai/chat"; 5 5 import { generateChatTitle } from "../lib/ai/title"; ··· 7 7 import { db, schema } from "../lib/db"; 8 8 import { nanoid } from "../lib/id"; 9 9 import { chatSchema, messageSchema } from "../lib/schema"; 10 + import { requireAuth } from "../middlewares/auth"; 10 11 11 - const app = new Hono() 12 + const app = new Hono<{ Variables: { userId: string } }>() 13 + .use("*", requireAuth) 12 14 .get("/", async (c) => { 15 + const userId = c.get("userId"); 13 16 const res = await db.query.chats.findMany({ 17 + where: eq(schema.chats.userId, userId), 14 18 orderBy: desc(schema.chats.createdAt), 15 19 }); 16 20 ··· 18 22 }) 19 23 .get("/:id", async (c) => { 20 24 const { id } = c.req.param(); 25 + const userId = c.get("userId"); 21 26 22 27 const res = await db.query.chats.findFirst({ 23 - where: eq(schema.chats.id, id), 28 + where: and(eq(schema.chats.id, id), eq(schema.chats.userId, userId)), 24 29 with: { messages: true }, 25 30 }); 26 31 ··· 30 35 }) 31 36 .post("/", zValidator("json", chatSchema), async (c) => { 32 37 const body = c.req.valid("json"); 38 + const userId = c.get("userId"); 33 39 const id = nanoid(); 34 40 35 41 db.insert(schema.chats) 36 - .values({ id, ...body }) 42 + .values({ id, userId, ...body }) 37 43 .run(); 38 44 39 45 const chat = await db.query.chats.findFirst({ 40 - where: eq(schema.chats.id, id), 46 + where: and(eq(schema.chats.id, id), eq(schema.chats.userId, userId)), 41 47 }); 42 48 43 49 if (!chat) return c.json({ message: "Not found" }, 404); ··· 47 53 .post("/:id/messages", zValidator("json", messageSchema), async (c) => { 48 54 const { id } = c.req.param(); 49 55 const body = c.req.valid("json"); 50 - const messageId = nanoid(); 51 - 52 - db.insert(schema.messages) 53 - .values({ id: messageId, chatId: id, role: "user", ...body }) 54 - .run(); 56 + const userId = c.get("userId"); 55 57 56 58 const chat = await db.query.chats.findFirst({ 57 - where: eq(schema.chats.id, id), 59 + where: and(eq(schema.chats.id, id), eq(schema.chats.userId, userId)), 58 60 }); 59 61 60 62 if (!chat) return c.json({ message: "Not found" }, 404); 61 63 64 + db.insert(schema.messages) 65 + .values({ 66 + id: nanoid(), 67 + chatId: id, 68 + userId, 69 + role: "user", 70 + ...body, 71 + }) 72 + .run(); 73 + 62 74 const character = characters.find((c) => c.id === chat.characterId); 63 75 64 76 if (!character) return c.json({ message: "Invalid character" }, 400); 65 77 66 78 const messages = await db.query.messages.findMany({ 67 - where: eq(schema.messages.chatId, id), 79 + where: and( 80 + eq(schema.messages.chatId, id), 81 + eq(schema.messages.userId, userId), 82 + ), 68 83 orderBy: asc(schema.messages.createdAt), 69 84 }); 70 85 ··· 99 114 if (title) { 100 115 db.update(schema.chats) 101 116 .set({ title }) 102 - .where(eq(schema.chats.id, id)) 117 + .where( 118 + and(eq(schema.chats.id, id), eq(schema.chats.userId, userId)), 119 + ) 103 120 .run(); 104 121 } 105 122 } ··· 108 125 .values({ 109 126 id: nanoid(), 110 127 chatId: id, 128 + userId, 111 129 role: "assistant", 112 130 text: assistantText, 113 131 }) ··· 129 147 .patch("/:id", zValidator("json", chatSchema.partial()), async (c) => { 130 148 const { id } = c.req.param(); 131 149 const body = c.req.valid("json"); 150 + const userId = c.get("userId"); 132 151 133 - db.update(schema.chats).set(body).where(eq(schema.chats.id, id)).run(); 152 + db.update(schema.chats) 153 + .set(body) 154 + .where(and(eq(schema.chats.id, id), eq(schema.chats.userId, userId))) 155 + .run(); 134 156 135 157 const chat = await db.query.chats.findFirst({ 136 - where: eq(schema.chats.id, id), 158 + where: and(eq(schema.chats.id, id), eq(schema.chats.userId, userId)), 137 159 }); 138 160 139 161 if (!chat) return c.json({ message: "Not found" }, 404); ··· 142 164 }) 143 165 .delete("/:id", (c) => { 144 166 const { id } = c.req.param(); 167 + const userId = c.get("userId"); 145 168 146 - db.delete(schema.chats).where(eq(schema.chats.id, id)).run(); 169 + db.delete(schema.chats) 170 + .where(and(eq(schema.chats.id, id), eq(schema.chats.userId, userId))) 171 + .run(); 147 172 148 173 return c.json({ id, deleted: true }); 149 174 });
+4 -1
apps/web/package.json
··· 10 10 "dependencies": { 11 11 "@base-ui/react": "^1.4.1", 12 12 "@cai/api": "workspace:*", 13 + "@clerk/clerk-react": "^5.61.3", 13 14 "@fontsource-variable/dm-sans": "^5.2.8", 15 + "@t3-oss/env-core": "^0.13.11", 14 16 "@tailwindcss/vite": "^4.1.18", 15 17 "@tanstack/react-query": "^5.100.5", 16 18 "@tanstack/react-router": "latest", ··· 23 25 "react-markdown": "^10.1.0", 24 26 "shadcn": "^4.5.0", 25 27 "tailwindcss": "^4.1.18", 26 - "tw-animate-css": "^1.4.0" 28 + "tw-animate-css": "^1.4.0", 29 + "zod": "^4.3.6" 27 30 }, 28 31 "devDependencies": { 29 32 "@tailwindcss/typography": "^0.5.16",
+8 -6
apps/web/src/components/chat-messages.tsx
··· 28 28 29 29 useEffect(() => { 30 30 const container = containerRef.current; 31 - if (!container) return; 31 + if (!(container instanceof HTMLDivElement)) return; 32 + const containerElement: HTMLDivElement = container; 32 33 33 34 function updateShouldStickToBottom() { 34 35 shouldStickToBottomRef.current = 35 - getDistanceFromBottom(container) <= SCROLL_THRESHOLD_PX; 36 + getDistanceFromBottom(containerElement) <= SCROLL_THRESHOLD_PX; 36 37 } 37 38 38 39 updateShouldStickToBottom(); 39 - container.addEventListener("scroll", updateShouldStickToBottom); 40 + containerElement.addEventListener("scroll", updateShouldStickToBottom); 40 41 41 42 return () => { 42 - container.removeEventListener("scroll", updateShouldStickToBottom); 43 + containerElement.removeEventListener("scroll", updateShouldStickToBottom); 43 44 }; 44 45 }, []); 45 46 46 47 useLayoutEffect(() => { 47 48 const container = containerRef.current; 48 - if (!container || messages.length === 0) return; 49 + if (!(container instanceof HTMLDivElement) || messages.length === 0) return; 50 + const containerElement: HTMLDivElement = container; 49 51 50 52 const previousMessages = previousMessagesRef.current; 51 53 const previousLastMessage = previousMessages.at(-1); ··· 63 65 } 64 66 65 67 if (shouldForceScroll || shouldStickToBottomRef.current) { 66 - scrollToBottom(container); 68 + scrollToBottom(containerElement); 67 69 shouldStickToBottomRef.current = true; 68 70 } 69 71
+8
apps/web/src/components/chat-sidebar.tsx
··· 1 + import { SignedIn } from "@clerk/clerk-react"; 1 2 import { useChats } from "#/hooks/use-chats"; 2 3 import { ChatSidebarHeader } from "./chat-sidebar-header"; 3 4 import { ChatSidebarItem } from "./chat-sidebar-item"; 5 + import { SidebarUserButton } from "./sidebar-user-button"; 4 6 import { 5 7 Sidebar, 6 8 SidebarContent, 9 + SidebarFooter, 7 10 SidebarGroup, 8 11 SidebarGroupLabel, 9 12 SidebarMenu, ··· 25 28 </SidebarMenu> 26 29 </SidebarGroup> 27 30 </SidebarContent> 31 + <SidebarFooter> 32 + <SignedIn> 33 + <SidebarUserButton /> 34 + </SignedIn> 35 + </SidebarFooter> 28 36 </Sidebar> 29 37 ); 30 38 }
+33 -22
apps/web/src/components/layout.tsx
··· 1 1 import { cva, type VariantProps } from "class-variance-authority"; 2 + import { forwardRef } from "react"; 2 3 import { cn } from "#/lib/utils"; 3 4 4 5 const layout = cva("flex", { ··· 33 34 type RowProps = React.HTMLAttributes<HTMLDivElement> & 34 35 VariantProps<typeof layout>; 35 36 36 - export function Stack({ gap, items, justify, className, ...props }: RowProps) { 37 - return ( 38 - <div 39 - className={cn( 40 - layout({ direction: "vertical", gap, items, justify }), 41 - className, 42 - )} 43 - {...props} 44 - /> 45 - ); 46 - } 37 + export const Stack = forwardRef<HTMLDivElement, RowProps>( 38 + ({ gap, items, justify, className, ...props }, ref) => { 39 + return ( 40 + <div 41 + ref={ref} 42 + className={cn( 43 + layout({ direction: "vertical", gap, items, justify }), 44 + className, 45 + )} 46 + {...props} 47 + /> 48 + ); 49 + }, 50 + ); 47 51 48 - export function Row({ gap, items, justify, className, ...props }: RowProps) { 49 - return ( 50 - <div 51 - className={cn( 52 - layout({ direction: "horizontal", gap, items, justify }), 53 - className, 54 - )} 55 - {...props} 56 - /> 57 - ); 58 - } 52 + Stack.displayName = "Stack"; 53 + 54 + export const Row = forwardRef<HTMLDivElement, RowProps>( 55 + ({ gap, items, justify, className, ...props }, ref) => { 56 + return ( 57 + <div 58 + ref={ref} 59 + className={cn( 60 + layout({ direction: "horizontal", gap, items, justify }), 61 + className, 62 + )} 63 + {...props} 64 + /> 65 + ); 66 + }, 67 + ); 68 + 69 + Row.displayName = "Row";
+71
apps/web/src/components/sidebar-user-button.tsx
··· 1 + import { useClerk, useUser } from "@clerk/clerk-react"; 2 + import { ChevronUpIcon, LogOutIcon, SettingsIcon } from "lucide-react"; 3 + import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"; 4 + import { 5 + DropdownMenu, 6 + DropdownMenuContent, 7 + DropdownMenuItem, 8 + DropdownMenuSeparator, 9 + DropdownMenuTrigger, 10 + } from "./ui/dropdown-menu"; 11 + import { SidebarMenu, SidebarMenuButton, SidebarMenuItem } from "./ui/sidebar"; 12 + 13 + function getUserName(user: ReturnType<typeof useUser>["user"]) { 14 + if (!user) return "Account"; 15 + 16 + return ( 17 + user.fullName || 18 + [user.firstName, user.lastName].filter(Boolean).join(" ") || 19 + user.primaryEmailAddress?.emailAddress || 20 + "Account" 21 + ); 22 + } 23 + 24 + function getInitials(name: string) { 25 + return name 26 + .split(" ") 27 + .filter(Boolean) 28 + .slice(0, 2) 29 + .map((part) => part[0]?.toUpperCase() ?? "") 30 + .join(""); 31 + } 32 + 33 + export function SidebarUserButton() { 34 + const clerk = useClerk(); 35 + const { user } = useUser(); 36 + const name = getUserName(user); 37 + const initials = getInitials(name); 38 + 39 + return ( 40 + <SidebarMenu> 41 + <SidebarMenuItem> 42 + <DropdownMenu> 43 + <DropdownMenuTrigger render={<SidebarMenuButton size="lg" />}> 44 + <Avatar> 45 + <AvatarImage src={user?.imageUrl} alt={name} /> 46 + <AvatarFallback>{initials}</AvatarFallback> 47 + </Avatar> 48 + <span className="flex flex-1 items-center gap-2"> 49 + <span className="truncate">{name}</span> 50 + </span> 51 + <ChevronUpIcon className="ml-auto" /> 52 + </DropdownMenuTrigger> 53 + <DropdownMenuContent side="top" align="end"> 54 + <DropdownMenuItem onClick={() => clerk.openUserProfile()}> 55 + <SettingsIcon /> 56 + Account settings 57 + </DropdownMenuItem> 58 + <DropdownMenuSeparator /> 59 + <DropdownMenuItem 60 + variant="destructive" 61 + onClick={() => void clerk.signOut()} 62 + > 63 + <LogOutIcon /> 64 + Sign out 65 + </DropdownMenuItem> 66 + </DropdownMenuContent> 67 + </DropdownMenu> 68 + </SidebarMenuItem> 69 + </SidebarMenu> 70 + ); 71 + }
+11
apps/web/src/env.ts
··· 1 + import { createEnv } from "@t3-oss/env-core"; 2 + import { z } from "zod"; 3 + 4 + export const env = createEnv({ 5 + clientPrefix: "VITE_", 6 + client: { 7 + VITE_CLERK_PUBLISHABLE_KEY: z.string().min(1), 8 + }, 9 + runtimeEnv: import.meta.env, 10 + emptyStringAsUndefined: true, 11 + });
+6 -1
apps/web/src/hooks/use-chat.ts
··· 1 + import { useAuth } from "@clerk/clerk-react"; 1 2 import { useQuery } from "@tanstack/react-query"; 2 3 import { client } from "#/lib/api"; 4 + import { chatQueryKey } from "#/lib/chat-query"; 3 5 4 6 interface UseChatOptions { 5 7 id: string; 6 8 } 7 9 8 10 export function useChat({ id }: UseChatOptions) { 11 + const { isLoaded, isSignedIn, userId } = useAuth(); 12 + 9 13 const chatQuery = useQuery({ 10 - queryKey: ["chat", id], 14 + enabled: isLoaded && isSignedIn, 15 + queryKey: chatQueryKey(userId, id), 11 16 queryFn: async () => { 12 17 const response = await client.api.chats[":id"].$get({ 13 18 param: { id },
+6 -1
apps/web/src/hooks/use-chats.ts
··· 1 + import { useAuth } from "@clerk/clerk-react"; 1 2 import { useQuery } from "@tanstack/react-query"; 2 3 import { client } from "#/lib/api"; 4 + import { chatsQueryKey } from "#/lib/chat-query"; 3 5 4 6 export function useChats() { 7 + const { isLoaded, isSignedIn, userId } = useAuth(); 8 + 5 9 return useQuery({ 6 - queryKey: ["chats"], 10 + enabled: isLoaded && isSignedIn, 11 + queryKey: chatsQueryKey(userId), 7 12 queryFn: async () => { 8 13 const response = await client.api.chats.$get(); 9 14
+7 -3
apps/web/src/hooks/use-delete-chat.ts
··· 1 + import { useAuth } from "@clerk/clerk-react"; 1 2 import { useMutation, useQueryClient } from "@tanstack/react-query"; 2 3 import { useNavigate } from "@tanstack/react-router"; 3 4 import { client } from "#/lib/api"; 5 + import { chatQueryKey, chatsQueryKey } from "#/lib/chat-query"; 4 6 import type { ChatSummary } from "#/lib/types"; 5 7 6 8 export function useDeleteChat() { 9 + const { userId } = useAuth(); 7 10 const queryClient = useQueryClient(); 8 11 const navigate = useNavigate(); 9 12 ··· 20 23 return id; 21 24 }, 22 25 onSuccess: async (id) => { 23 - queryClient.setQueryData<ChatSummary[]>(["chats"], (chats = []) => 24 - chats.filter((chat) => chat.id !== id), 26 + queryClient.setQueryData<ChatSummary[]>( 27 + chatsQueryKey(userId), 28 + (chats = []) => chats.filter((chat) => chat.id !== id), 25 29 ); 26 - queryClient.removeQueries({ queryKey: ["chat", id] }); 30 + queryClient.removeQueries({ queryKey: chatQueryKey(userId, id) }); 27 31 28 32 const currentPath = window.location.pathname; 29 33 if (currentPath === `/chats/${id}`) {
+11 -6
apps/web/src/hooks/use-rename-chat.ts
··· 1 + import { useAuth } from "@clerk/clerk-react"; 1 2 import { useMutation, useQueryClient } from "@tanstack/react-query"; 2 3 import { client } from "#/lib/api"; 4 + import { chatsQueryKey } from "#/lib/chat-query"; 3 5 import type { ChatSummary } from "#/lib/types"; 4 6 5 7 export function useRenameChat() { 8 + const { userId } = useAuth(); 6 9 const queryClient = useQueryClient(); 7 10 8 11 return useMutation({ ··· 19 22 return response.json(); 20 23 }, 21 24 onSuccess: (updatedChat) => { 22 - queryClient.setQueryData<ChatSummary[]>(["chats"], (chats = []) => 23 - chats.map((chat) => 24 - chat.id === updatedChat.id 25 - ? { ...chat, title: updatedChat.title } 26 - : chat, 27 - ), 25 + queryClient.setQueryData<ChatSummary[]>( 26 + chatsQueryKey(userId), 27 + (chats = []) => 28 + chats.map((chat) => 29 + chat.id === updatedChat.id 30 + ? { ...chat, title: updatedChat.title } 31 + : chat, 32 + ), 28 33 ); 29 34 }, 30 35 });
+61 -30
apps/web/src/hooks/use-send-message.ts
··· 1 + import { useAuth } from "@clerk/clerk-react"; 1 2 import { 2 3 type QueryClient, 3 4 useMutation, ··· 5 6 } from "@tanstack/react-query"; 6 7 import { useNavigate } from "@tanstack/react-router"; 7 8 import { client } from "#/lib/api"; 9 + import { chatQueryKey, chatsQueryKey } from "#/lib/chat-query"; 8 10 import type { 9 11 CharacterId, 10 12 Chat, ··· 13 15 ChatSummary, 14 16 } from "#/lib/types"; 15 17 16 - const chatQueryKey = (chatId: string) => ["chat", chatId] as const; 17 - const chatsQueryKey = ["chats"] as const; 18 - 19 18 interface SendMessageInput { 20 19 chatId?: string; 21 20 characterId: CharacterId | null; ··· 23 22 text: string; 24 23 } 25 24 26 - function createMessage(chatId: string, role: ChatMessage["role"], text = "") { 25 + function createMessage( 26 + chatId: string, 27 + userId: string | null | undefined, 28 + role: ChatMessage["role"], 29 + text = "", 30 + ) { 27 31 return { 28 32 id: `optimistic-${role}-${crypto.randomUUID()}`, 29 33 chatId, 34 + userId: userId ?? null, 30 35 role, 31 36 text, 32 37 createdAt: new Date().toISOString(), ··· 35 40 36 41 function setMessages( 37 42 queryClient: QueryClient, 43 + userId: string | null | undefined, 38 44 chatId: string, 39 45 update: (messages: ChatMessage[]) => ChatMessage[], 40 46 ) { 41 - queryClient.setQueryData<Chat | undefined>(chatQueryKey(chatId), (chat) => { 42 - if (!chat) return chat; 47 + queryClient.setQueryData<Chat | undefined>( 48 + chatQueryKey(userId, chatId), 49 + (chat) => { 50 + if (!chat) return chat; 43 51 44 - return { 45 - ...chat, 46 - messages: update(chat.messages), 47 - }; 48 - }); 52 + return { 53 + ...chat, 54 + messages: update(chat.messages), 55 + }; 56 + }, 57 + ); 49 58 } 50 59 51 60 function patchMessage( 52 61 queryClient: QueryClient, 62 + userId: string | null | undefined, 53 63 chatId: string, 54 64 messageId: string, 55 65 update: (message: ChatMessage) => ChatMessage, 56 66 ) { 57 - setMessages(queryClient, chatId, (messages) => 67 + setMessages(queryClient, userId, chatId, (messages) => 58 68 messages.map((message) => 59 69 message.id === messageId ? update(message) : message, 60 70 ), 61 71 ); 62 72 } 63 73 64 - function addChatToList(queryClient: QueryClient, chat: ChatSummary) { 74 + function addChatToList( 75 + queryClient: QueryClient, 76 + userId: string | null | undefined, 77 + chat: ChatSummary, 78 + ) { 65 79 queryClient.setQueryData<ChatSummary[] | undefined>( 66 - chatsQueryKey, 80 + chatsQueryKey(userId), 67 81 (chats = []) => { 68 82 if (chats.some((currentChat) => currentChat.id === chat.id)) { 69 83 return chats; ··· 115 129 } 116 130 117 131 export function useSendMessage() { 132 + const { userId } = useAuth(); 118 133 const navigate = useNavigate(); 119 134 const queryClient = useQueryClient(); 120 135 ··· 143 158 144 159 const chat = await response.json(); 145 160 146 - queryClient.setQueryData<Chat>(chatQueryKey(chat.id), { 161 + queryClient.setQueryData<Chat>(chatQueryKey(userId, chat.id), { 147 162 ...chat, 148 163 messages: [], 149 164 }); 150 - addChatToList(queryClient, chat); 165 + addChatToList(queryClient, userId, chat); 151 166 await navigate({ to: "/chats/$chatId", params: { chatId: chat.id } }); 152 167 153 168 return chat.id; ··· 172 187 modelId, 173 188 }); 174 189 175 - await queryClient.cancelQueries({ queryKey: chatQueryKey(id) }); 190 + await queryClient.cancelQueries({ queryKey: chatQueryKey(userId, id) }); 176 191 177 - const userMessage = createMessage(id, "user", trimmedText); 178 - const assistantMessage = createMessage(id, "assistant"); 192 + const userMessage = createMessage(id, userId, "user", trimmedText); 193 + const assistantMessage = createMessage(id, userId, "assistant"); 179 194 180 - setMessages(queryClient, id, (messages) => [ 195 + setMessages(queryClient, userId, id, (messages) => [ 181 196 ...messages, 182 197 userMessage, 183 198 assistantMessage, ··· 186 201 try { 187 202 const response = await sendMessageToChat(id, trimmedText); 188 203 await readTextStream(response, (chunk) => { 189 - patchMessage(queryClient, id, assistantMessage.id, (message) => ({ 190 - ...message, 191 - text: `${message.text}${chunk}`, 192 - })); 204 + patchMessage( 205 + queryClient, 206 + userId, 207 + id, 208 + assistantMessage.id, 209 + (message) => ({ 210 + ...message, 211 + text: `${message.text}${chunk}`, 212 + }), 213 + ); 193 214 }); 194 215 } catch (error) { 195 - patchMessage(queryClient, id, assistantMessage.id, (message) => ({ 196 - ...message, 197 - text: "Failed to stream response.", 198 - })); 216 + patchMessage( 217 + queryClient, 218 + userId, 219 + id, 220 + assistantMessage.id, 221 + (message) => ({ 222 + ...message, 223 + text: "Failed to stream response.", 224 + }), 225 + ); 199 226 throw error; 200 227 } finally { 201 - await queryClient.invalidateQueries({ queryKey: chatQueryKey(id) }); 202 - await queryClient.invalidateQueries({ queryKey: chatsQueryKey }); 228 + await queryClient.invalidateQueries({ 229 + queryKey: chatQueryKey(userId, id), 230 + }); 231 + await queryClient.invalidateQueries({ 232 + queryKey: chatsQueryKey(userId), 233 + }); 203 234 } 204 235 205 236 return { id };
+12 -7
apps/web/src/hooks/use-update-chat-character.ts
··· 1 + import { useAuth } from "@clerk/clerk-react"; 1 2 import { useMutation, useQueryClient } from "@tanstack/react-query"; 2 3 import { client } from "#/lib/api"; 4 + import { chatQueryKey, chatsQueryKey } from "#/lib/chat-query"; 3 5 import type { 4 6 Chat, 5 7 ChatSummary, ··· 11 13 Pick<PatchChatJson, "characterId">; 12 14 13 15 export function useUpdateChatCharacter() { 16 + const { userId } = useAuth(); 14 17 const queryClient = useQueryClient(); 15 18 16 19 return useMutation({ ··· 27 30 return response.json(); 28 31 }, 29 32 onSuccess: (updatedChat) => { 30 - queryClient.setQueryData<ChatSummary[]>(["chats"], (chats = []) => 31 - chats.map((chat) => 32 - chat.id === updatedChat.id 33 - ? { ...chat, characterId: updatedChat.characterId } 34 - : chat, 35 - ), 33 + queryClient.setQueryData<ChatSummary[]>( 34 + chatsQueryKey(userId), 35 + (chats = []) => 36 + chats.map((chat) => 37 + chat.id === updatedChat.id 38 + ? { ...chat, characterId: updatedChat.characterId } 39 + : chat, 40 + ), 36 41 ); 37 42 38 43 queryClient.setQueryData<Chat | undefined>( 39 - ["chat", updatedChat.id], 44 + chatQueryKey(userId, updatedChat.id), 40 45 (chat) => 41 46 chat ? { ...chat, characterId: updatedChat.characterId } : chat, 42 47 );
+12 -7
apps/web/src/hooks/use-update-chat-model.ts
··· 1 + import { useAuth } from "@clerk/clerk-react"; 1 2 import { useMutation, useQueryClient } from "@tanstack/react-query"; 2 3 import { client } from "#/lib/api"; 4 + import { chatQueryKey, chatsQueryKey } from "#/lib/chat-query"; 3 5 import type { 4 6 Chat, 5 7 ChatSummary, ··· 10 12 type UpdateChatModelInput = PatchChatParam & Pick<PatchChatJson, "modelId">; 11 13 12 14 export function useUpdateChatModel() { 15 + const { userId } = useAuth(); 13 16 const queryClient = useQueryClient(); 14 17 15 18 return useMutation({ ··· 26 29 return response.json(); 27 30 }, 28 31 onSuccess: (updatedChat) => { 29 - queryClient.setQueryData<ChatSummary[]>(["chats"], (chats = []) => 30 - chats.map((chat) => 31 - chat.id === updatedChat.id 32 - ? { ...chat, modelId: updatedChat.modelId } 33 - : chat, 34 - ), 32 + queryClient.setQueryData<ChatSummary[]>( 33 + chatsQueryKey(userId), 34 + (chats = []) => 35 + chats.map((chat) => 36 + chat.id === updatedChat.id 37 + ? { ...chat, modelId: updatedChat.modelId } 38 + : chat, 39 + ), 35 40 ); 36 41 37 42 queryClient.setQueryData<Chat | undefined>( 38 - ["chat", updatedChat.id], 43 + chatQueryKey(userId, updatedChat.id), 39 44 (chat) => (chat ? { ...chat, modelId: updatedChat.modelId } : chat), 40 45 ); 41 46 },
+23 -1
apps/web/src/lib/api.ts
··· 1 1 import type { AppType } from "@cai/api"; 2 2 import { hc } from "hono/client"; 3 3 4 - export const client = hc<AppType>("/"); 4 + type TokenGetter = () => Promise<string | null>; 5 + 6 + let getToken: TokenGetter | undefined; 7 + 8 + export function setAuthTokenGetter(nextGetter?: TokenGetter) { 9 + getToken = nextGetter; 10 + } 11 + 12 + export const client = hc<AppType>("/", { 13 + init: { 14 + credentials: "include", 15 + }, 16 + headers: async () => { 17 + const token = await getToken?.(); 18 + const headers: Record<string, string> = {}; 19 + 20 + if (token) { 21 + headers.Authorization = `Bearer ${token}`; 22 + } 23 + 24 + return headers; 25 + }, 26 + });
+7
apps/web/src/lib/chat-query.ts
··· 1 + export const chatsQueryKey = (userId: string | null | undefined) => 2 + ["chats", userId ?? "anonymous"] as const; 3 + 4 + export const chatQueryKey = ( 5 + userId: string | null | undefined, 6 + chatId: string, 7 + ) => ["chat", userId ?? "anonymous", chatId] as const;
+6 -1
apps/web/src/main.tsx
··· 1 + import { ClerkProvider } from "@clerk/clerk-react"; 1 2 import { RouterProvider } from "@tanstack/react-router"; 2 3 import { StrictMode } from "react"; 3 4 import { createRoot } from "react-dom/client"; 5 + import { env } from "./env"; 4 6 import { getRouter } from "./router"; 5 7 import "./styles.css"; 6 8 7 9 const rootElement = document.getElementById("app"); 10 + const publishableKey = env.VITE_CLERK_PUBLISHABLE_KEY; 8 11 9 12 if (!rootElement) { 10 13 throw new Error("Missing root element #app"); ··· 12 15 13 16 createRoot(rootElement).render( 14 17 <StrictMode> 15 - <RouterProvider router={getRouter()} /> 18 + <ClerkProvider publishableKey={publishableKey}> 19 + <RouterProvider router={getRouter()} /> 20 + </ClerkProvider> 16 21 </StrictMode>, 17 22 );
+28
apps/web/src/routes/__root.tsx
··· 1 + import { useAuth } from "@clerk/clerk-react"; 1 2 import { QueryClientProvider } from "@tanstack/react-query"; 2 3 import { createRootRoute, Outlet } from "@tanstack/react-router"; 4 + import { useEffect, useRef } from "react"; 5 + import { setAuthTokenGetter } from "#/lib/api"; 3 6 import { queryClient } from "#/lib/query"; 4 7 5 8 export const Route = createRootRoute({ 6 9 component: Root, 7 10 }); 8 11 12 + function AuthSync() { 13 + const { getToken, isLoaded, userId } = useAuth(); 14 + const previousUserId = useRef<string | null | undefined>(undefined); 15 + 16 + useEffect(() => { 17 + setAuthTokenGetter(async () => (isLoaded ? getToken() : null)); 18 + 19 + return () => setAuthTokenGetter(undefined); 20 + }, [getToken, isLoaded]); 21 + 22 + useEffect(() => { 23 + if (!isLoaded) { 24 + return; 25 + } 26 + 27 + if (previousUserId.current !== userId) { 28 + queryClient.clear(); 29 + previousUserId.current = userId; 30 + } 31 + }, [isLoaded, userId]); 32 + 33 + return null; 34 + } 35 + 9 36 function Root() { 10 37 return ( 11 38 <QueryClientProvider client={queryClient}> 39 + <AuthSync /> 12 40 <Outlet /> 13 41 </QueryClientProvider> 14 42 );
+11
apps/web/src/routes/chats.tsx
··· 1 + import { RedirectToSignIn, useAuth } from "@clerk/clerk-react"; 1 2 import { createFileRoute, Outlet } from "@tanstack/react-router"; 2 3 import { ChatSidebar } from "#/components/chat-sidebar"; 3 4 import { Container } from "#/components/container"; ··· 8 9 }); 9 10 10 11 function RouteComponent() { 12 + const { isLoaded, isSignedIn } = useAuth(); 13 + 14 + if (!isLoaded) { 15 + return null; 16 + } 17 + 18 + if (!isSignedIn) { 19 + return <RedirectToSignIn />; 20 + } 21 + 11 22 return ( 12 23 <SidebarProvider className="h-dvh"> 13 24 <ChatSidebar />
+32
bun.lock
··· 13 13 "name": "@cai/api", 14 14 "dependencies": { 15 15 "@ai-sdk/openai": "^3.0.53", 16 + "@clerk/backend": "^3.4.1", 16 17 "@hono/zod-validator": "^0.7.6", 17 18 "ai": "^6.0.168", 18 19 "drizzle-orm": "^0.45.2", ··· 31 32 "dependencies": { 32 33 "@base-ui/react": "^1.4.1", 33 34 "@cai/api": "workspace:*", 35 + "@clerk/clerk-react": "^5.61.3", 34 36 "@fontsource-variable/dm-sans": "^5.2.8", 37 + "@t3-oss/env-core": "^0.13.11", 35 38 "@tailwindcss/vite": "^4.1.18", 36 39 "@tanstack/react-query": "^5.100.5", 37 40 "@tanstack/react-router": "latest", ··· 45 48 "shadcn": "^4.5.0", 46 49 "tailwindcss": "^4.1.18", 47 50 "tw-animate-css": "^1.4.0", 51 + "zod": "^4.3.6", 48 52 }, 49 53 "devDependencies": { 50 54 "@tailwindcss/typography": "^0.5.16", ··· 151 155 152 156 "@cai/web": ["@cai/web@workspace:apps/web"], 153 157 158 + "@clerk/backend": ["@clerk/backend@3.4.1", "", { "dependencies": { "@clerk/shared": "^4.8.5", "standardwebhooks": "^1.0.0", "tslib": "2.8.1" } }, "sha512-+Tgo1uPEFpBRvyFW3JtPbrTMRgiP+pWBo9gi2tTB0AxEqR2I/kSYy5l3+KqWciUpbVZtVvLXm1j+NEE2WEG+jg=="], 159 + 160 + "@clerk/clerk-react": ["@clerk/clerk-react@5.61.3", "", { "dependencies": { "@clerk/shared": "^3.47.2", "tslib": "2.8.1" }, "peerDependencies": { "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" } }, "sha512-W21aNEeHtqh3xJLuW5g2ydben/1D5pSnxsl/kCnv0IY1zma7lO+aIJ7Br2bR4FKKkiu695mPnjtY+fvkQmCXBg=="], 161 + 162 + "@clerk/shared": ["@clerk/shared@4.8.5", "", { "dependencies": { "@tanstack/query-core": "5.90.16", "dequal": "2.0.3", "glob-to-regexp": "0.4.1", "js-cookie": "3.0.5", "std-env": "^3.9.0" }, "peerDependencies": { "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-YxgUWHoKEXEbRPWPEcB2Q0o+NJkDc0/zQRp4QCsnGIM5e32hlBUwxcYpyDjDlZ2lYB+GUXHuEc3KETnxWGp26g=="], 163 + 154 164 "@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.63.0", "", { "dependencies": { "commander": "^11.1.0", "dotenv": "^17.2.1", "eciesjs": "^0.4.10", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "picomatch": "^4.0.4", "which": "^4.0.0", "yocto-spinner": "^1.1.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js" } }, "sha512-jjkmzIRu19uH78AjFInqfcALehbDCZZ7M09hurVawyqNxtOXEg2LR73L59y4QnzfYDEzjbhVzGAd2uDHu0D1aQ=="], 155 165 156 166 "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], ··· 345 355 346 356 "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], 347 357 358 + "@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="], 359 + 348 360 "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], 361 + 362 + "@t3-oss/env-core": ["@t3-oss/env-core@0.13.11", "", { "peerDependencies": { "arktype": "^2.1.0", "typescript": ">=5.0.0", "valibot": "^1.0.0-beta.7 || ^1.0.0", "zod": "^3.24.0 || ^4.0.0" }, "optionalPeers": ["arktype", "typescript", "valibot", "zod"] }, "sha512-sM7GYY+KL7H/Hl0BE0inWfk3nRHZOLhmVn7sHGxaZt9FAR6KqREXAE+6TqKfiavfXmpRxO/OZ2QgKRd+oiBYRQ=="], 349 363 350 364 "@tailwindcss/node": ["@tailwindcss/node@4.2.4", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.4" } }, "sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA=="], 351 365 ··· 638 652 "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], 639 653 640 654 "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], 655 + 656 + "fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="], 641 657 642 658 "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], 643 659 ··· 691 707 692 708 "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], 693 709 710 + "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], 711 + 694 712 "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], 695 713 696 714 "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], ··· 784 802 "jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], 785 803 786 804 "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="], 805 + 806 + "js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="], 787 807 788 808 "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], 789 809 ··· 1095 1115 1096 1116 "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], 1097 1117 1118 + "standardwebhooks": ["standardwebhooks@1.0.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg=="], 1119 + 1098 1120 "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], 1121 + 1122 + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], 1099 1123 1100 1124 "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], 1101 1125 ··· 1117 1141 1118 1142 "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], 1119 1143 1144 + "swr": ["swr@2.3.4", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg=="], 1145 + 1120 1146 "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], 1121 1147 1122 1148 "tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="], ··· 1229 1255 1230 1256 "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], 1231 1257 1258 + "@clerk/clerk-react/@clerk/shared": ["@clerk/shared@3.47.5", "", { "dependencies": { "csstype": "3.1.3", "dequal": "2.0.3", "glob-to-regexp": "0.4.1", "js-cookie": "3.0.5", "std-env": "^3.9.0", "swr": "2.3.4" }, "peerDependencies": { "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-rDVe73/VN2NZXhtrLRHshkUpQDrevAqDRxeXUl2M0IBEBkcl+VMHlV7fep53cVWo0b3gIqLk82pmmi+WoyF/xg=="], 1259 + 1260 + "@clerk/shared/@tanstack/query-core": ["@tanstack/query-core@5.90.16", "", {}, "sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww=="], 1261 + 1232 1262 "@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], 1233 1263 1234 1264 "@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], ··· 1298 1328 "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], 1299 1329 1300 1330 "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], 1331 + 1332 + "@clerk/clerk-react/@clerk/shared/csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], 1301 1333 1302 1334 "@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], 1303 1335