diff --git a/llama_stack/ui/app/api/auth/[...nextauth]/route.ts b/llama_stack/ui/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 000000000..7b38c1bb4 --- /dev/null +++ b/llama_stack/ui/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,6 @@ +import NextAuth from "next-auth"; +import { authOptions } from "@/lib/auth"; + +const handler = NextAuth(authOptions); + +export { handler as GET, handler as POST }; diff --git a/llama_stack/ui/app/auth/signin/page.tsx b/llama_stack/ui/app/auth/signin/page.tsx new file mode 100644 index 000000000..c9510fd6b --- /dev/null +++ b/llama_stack/ui/app/auth/signin/page.tsx @@ -0,0 +1,118 @@ +"use client"; + +import { signIn, signOut, useSession } from "next-auth/react"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Copy, Check, Home, Github } from "lucide-react"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; + +export default function SignInPage() { + const { data: session, status } = useSession(); + const [copied, setCopied] = useState(false); + const router = useRouter(); + + const handleCopyToken = async () => { + if (session?.accessToken) { + await navigator.clipboard.writeText(session.accessToken); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + } + }; + + if (status === "loading") { + return ( +
+
Loading...
+
+ ); + } + + return ( +
+ + + Authentication + + {session + ? "You are successfully authenticated!" + : "Sign in with GitHub to use your access token as an API key"} + + + + {!session ? ( + + ) : ( +
+
+ Signed in as {session.user?.email} +
+ + {session.accessToken && ( +
+
+ GitHub Access Token: +
+
+ + {session.accessToken} + + +
+
+ This GitHub token will be used as your API key for + authenticated Llama Stack requests. +
+
+ )} + +
+ + +
+
+ )} +
+
+
+ ); +} diff --git a/llama_stack/ui/app/layout.tsx b/llama_stack/ui/app/layout.tsx index ed8a6cd5d..19fb18c36 100644 --- a/llama_stack/ui/app/layout.tsx +++ b/llama_stack/ui/app/layout.tsx @@ -1,5 +1,6 @@ import type { Metadata } from "next"; import { ThemeProvider } from "@/components/ui/theme-provider"; +import { SessionProvider } from "@/components/providers/session-provider"; import { Geist, Geist_Mono } from "next/font/google"; import { ModeToggle } from "@/components/ui/mode-toggle"; import "./globals.css"; @@ -21,34 +22,38 @@ export const metadata: Metadata = { import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; import { AppSidebar } from "@/components/layout/app-sidebar"; +import { SignInButton } from "@/components/ui/sign-in-button"; export default function Layout({ children }: { children: React.ReactNode }) { return ( - - - -
- {/* Header with aligned elements */} -
-
- + + + + +
+ {/* Header with aligned elements */} +
+
+ +
+
+
+ + +
-
-
- -
-
-
{children}
-
-
-
+
{children}
+ + + + ); diff --git a/llama_stack/ui/app/logs/chat-completions/[id]/page.tsx b/llama_stack/ui/app/logs/chat-completions/[id]/page.tsx index e6feef363..82aa3496e 100644 --- a/llama_stack/ui/app/logs/chat-completions/[id]/page.tsx +++ b/llama_stack/ui/app/logs/chat-completions/[id]/page.tsx @@ -4,11 +4,12 @@ import { useEffect, useState } from "react"; import { useParams } from "next/navigation"; import { ChatCompletion } from "@/lib/types"; import { ChatCompletionDetailView } from "@/components/chat-completions/chat-completion-detail"; -import { client } from "@/lib/client"; +import { useAuthClient } from "@/hooks/use-auth-client"; export default function ChatCompletionDetailPage() { const params = useParams(); const id = params.id as string; + const client = useAuthClient(); const [completionDetail, setCompletionDetail] = useState(null); @@ -45,7 +46,7 @@ export default function ChatCompletionDetailPage() { }; fetchCompletionDetail(); - }, [id]); + }, [id, client]); return ( ( null, @@ -109,7 +110,7 @@ export default function ResponseDetailPage() { }; fetchResponseDetail(); - }, [id]); + }, [id, client]); return ( ({ }), })); +// Mock next-auth +jest.mock("next-auth/react", () => ({ + useSession: () => ({ + status: "authenticated", + data: { accessToken: "mock-token" }, + }), +})); + // Mock helper functions jest.mock("@/lib/truncate-text"); jest.mock("@/lib/format-message-content"); -// Mock the client -jest.mock("@/lib/client", () => ({ - client: { - chat: { - completions: { - list: jest.fn(), - }, +// Mock the auth client hook +const mockClient = { + chat: { + completions: { + list: jest.fn(), }, }, +}; + +jest.mock("@/hooks/use-auth-client", () => ({ + useAuthClient: () => mockClient, })); // Mock the usePagination hook const mockLoadMore = jest.fn(); -jest.mock("@/hooks/usePagination", () => ({ +jest.mock("@/hooks/use-pagination", () => ({ usePagination: jest.fn(() => ({ data: [], status: "idle", @@ -47,7 +57,7 @@ import { } from "@/lib/format-message-content"; // Import the mocked hook -import { usePagination } from "@/hooks/usePagination"; +import { usePagination } from "@/hooks/use-pagination"; const mockedUsePagination = usePagination as jest.MockedFunction< typeof usePagination >; diff --git a/llama_stack/ui/components/chat-completions/chat-completions-table.tsx b/llama_stack/ui/components/chat-completions/chat-completions-table.tsx index d5838d877..65f6c71af 100644 --- a/llama_stack/ui/components/chat-completions/chat-completions-table.tsx +++ b/llama_stack/ui/components/chat-completions/chat-completions-table.tsx @@ -10,8 +10,7 @@ import { extractTextFromContentPart, extractDisplayableText, } from "@/lib/format-message-content"; -import { usePagination } from "@/hooks/usePagination"; -import { client } from "@/lib/client"; +import { usePagination } from "@/hooks/use-pagination"; interface ChatCompletionsTableProps { /** Optional pagination configuration */ @@ -32,12 +31,15 @@ function formatChatCompletionToRow(completion: ChatCompletion): LogTableRow { export function ChatCompletionsTable({ paginationOptions, }: ChatCompletionsTableProps) { - const fetchFunction = async (params: { - after?: string; - limit: number; - model?: string; - order?: string; - }) => { + const fetchFunction = async ( + client: ReturnType, + params: { + after?: string; + limit: number; + model?: string; + order?: string; + }, + ) => { const response = await client.chat.completions.list({ after: params.after, limit: params.limit, diff --git a/llama_stack/ui/components/logs/logs-table-scroll.test.tsx b/llama_stack/ui/components/logs/logs-table-scroll.test.tsx index 0a143cc16..a5c3fde46 100644 --- a/llama_stack/ui/components/logs/logs-table-scroll.test.tsx +++ b/llama_stack/ui/components/logs/logs-table-scroll.test.tsx @@ -12,7 +12,7 @@ jest.mock("next/navigation", () => ({ })); // Mock the useInfiniteScroll hook -jest.mock("@/hooks/useInfiniteScroll", () => ({ +jest.mock("@/hooks/use-infinite-scroll", () => ({ useInfiniteScroll: jest.fn((onLoadMore, options) => { const ref = React.useRef(null); diff --git a/llama_stack/ui/components/logs/logs-table.tsx b/llama_stack/ui/components/logs/logs-table.tsx index 72924cea8..3d4e609c7 100644 --- a/llama_stack/ui/components/logs/logs-table.tsx +++ b/llama_stack/ui/components/logs/logs-table.tsx @@ -4,7 +4,7 @@ import { useRouter } from "next/navigation"; import { useRef } from "react"; import { truncateText } from "@/lib/truncate-text"; import { PaginationStatus } from "@/lib/types"; -import { useInfiniteScroll } from "@/hooks/useInfiniteScroll"; +import { useInfiniteScroll } from "@/hooks/use-infinite-scroll"; import { Table, TableBody, diff --git a/llama_stack/ui/components/providers/session-provider.tsx b/llama_stack/ui/components/providers/session-provider.tsx new file mode 100644 index 000000000..e194e5f0a --- /dev/null +++ b/llama_stack/ui/components/providers/session-provider.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { SessionProvider as NextAuthSessionProvider } from "next-auth/react"; + +export function SessionProvider({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/llama_stack/ui/components/responses/responses-table.test.tsx b/llama_stack/ui/components/responses/responses-table.test.tsx index 2286f10d5..0338b9151 100644 --- a/llama_stack/ui/components/responses/responses-table.test.tsx +++ b/llama_stack/ui/components/responses/responses-table.test.tsx @@ -12,21 +12,31 @@ jest.mock("next/navigation", () => ({ }), })); +// Mock next-auth +jest.mock("next-auth/react", () => ({ + useSession: () => ({ + status: "authenticated", + data: { accessToken: "mock-token" }, + }), +})); + // Mock helper functions jest.mock("@/lib/truncate-text"); -// Mock the client -jest.mock("@/lib/client", () => ({ - client: { - responses: { - list: jest.fn(), - }, +// Mock the auth client hook +const mockClient = { + responses: { + list: jest.fn(), }, +}; + +jest.mock("@/hooks/use-auth-client", () => ({ + useAuthClient: () => mockClient, })); // Mock the usePagination hook const mockLoadMore = jest.fn(); -jest.mock("@/hooks/usePagination", () => ({ +jest.mock("@/hooks/use-pagination", () => ({ usePagination: jest.fn(() => ({ data: [], status: "idle", @@ -40,7 +50,7 @@ jest.mock("@/hooks/usePagination", () => ({ import { truncateText as originalTruncateText } from "@/lib/truncate-text"; // Import the mocked hook -import { usePagination } from "@/hooks/usePagination"; +import { usePagination } from "@/hooks/use-pagination"; const mockedUsePagination = usePagination as jest.MockedFunction< typeof usePagination >; diff --git a/llama_stack/ui/components/responses/responses-table.tsx b/llama_stack/ui/components/responses/responses-table.tsx index 116a2adbe..a3e8c0c15 100644 --- a/llama_stack/ui/components/responses/responses-table.tsx +++ b/llama_stack/ui/components/responses/responses-table.tsx @@ -6,8 +6,7 @@ import { UsePaginationOptions, } from "@/lib/types"; import { LogsTable, LogTableRow } from "@/components/logs/logs-table"; -import { usePagination } from "@/hooks/usePagination"; -import { client } from "@/lib/client"; +import { usePagination } from "@/hooks/use-pagination"; import type { ResponseListResponse } from "llama-stack-client/resources/responses/responses"; import { isMessageInput, @@ -125,12 +124,15 @@ function formatResponseToRow(response: OpenAIResponse): LogTableRow { } export function ResponsesTable({ paginationOptions }: ResponsesTableProps) { - const fetchFunction = async (params: { - after?: string; - limit: number; - model?: string; - order?: string; - }) => { + const fetchFunction = async ( + client: ReturnType, + params: { + after?: string; + limit: number; + model?: string; + order?: string; + }, + ) => { const response = await client.responses.list({ after: params.after, limit: params.limit, diff --git a/llama_stack/ui/components/ui/sign-in-button.tsx b/llama_stack/ui/components/ui/sign-in-button.tsx new file mode 100644 index 000000000..caa15772f --- /dev/null +++ b/llama_stack/ui/components/ui/sign-in-button.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { User } from "lucide-react"; +import Link from "next/link"; +import { useSession } from "next-auth/react"; +import { Button } from "./button"; + +export function SignInButton() { + const { data: session, status } = useSession(); + + return ( + + ); +} diff --git a/llama_stack/ui/hooks/use-auth-client.ts b/llama_stack/ui/hooks/use-auth-client.ts new file mode 100644 index 000000000..94ae769f4 --- /dev/null +++ b/llama_stack/ui/hooks/use-auth-client.ts @@ -0,0 +1,24 @@ +import { useSession } from "next-auth/react"; +import { useMemo } from "react"; +import LlamaStackClient from "llama-stack-client"; + +export function useAuthClient() { + const { data: session } = useSession(); + + const client = useMemo(() => { + const clientHostname = + typeof window !== "undefined" ? window.location.origin : ""; + + const options: any = { + baseURL: `${clientHostname}/api`, + }; + + if (session?.accessToken) { + options.apiKey = session.accessToken; + } + + return new LlamaStackClient(options); + }, [session?.accessToken]); + + return client; +} diff --git a/llama_stack/ui/hooks/useInfiniteScroll.ts b/llama_stack/ui/hooks/use-infinite-scroll.ts similarity index 100% rename from llama_stack/ui/hooks/useInfiniteScroll.ts rename to llama_stack/ui/hooks/use-infinite-scroll.ts diff --git a/llama_stack/ui/hooks/usePagination.ts b/llama_stack/ui/hooks/use-pagination.ts similarity index 65% rename from llama_stack/ui/hooks/usePagination.ts rename to llama_stack/ui/hooks/use-pagination.ts index 55a8d4ac8..58847ece5 100644 --- a/llama_stack/ui/hooks/usePagination.ts +++ b/llama_stack/ui/hooks/use-pagination.ts @@ -2,6 +2,9 @@ import { useState, useCallback, useEffect, useRef } from "react"; import { PaginationStatus, UsePaginationOptions } from "@/lib/types"; +import { useSession } from "next-auth/react"; +import { useAuthClient } from "@/hooks/use-auth-client"; +import { useRouter } from "next/navigation"; interface PaginationState { data: T[]; @@ -28,13 +31,18 @@ export interface PaginationReturn { } interface UsePaginationParams extends UsePaginationOptions { - fetchFunction: (params: { - after?: string; - limit: number; - model?: string; - order?: string; - }) => Promise>; + fetchFunction: ( + client: ReturnType, + params: { + after?: string; + limit: number; + model?: string; + order?: string; + }, + ) => Promise>; errorMessagePrefix: string; + enabled?: boolean; + useAuth?: boolean; } export function usePagination({ @@ -43,7 +51,12 @@ export function usePagination({ order = "desc", fetchFunction, errorMessagePrefix, + enabled = true, + useAuth = true, }: UsePaginationParams): PaginationReturn { + const { status: sessionStatus } = useSession(); + const client = useAuthClient(); + const router = useRouter(); const [state, setState] = useState>({ data: [], status: "loading", @@ -74,7 +87,7 @@ export function usePagination({ error: null, })); - const response = await fetchFunction({ + const response = await fetchFunction(client, { after: after || undefined, limit: fetchLimit, ...(model && { model }), @@ -91,6 +104,17 @@ export function usePagination({ status: "idle", })); } catch (err) { + // Check if it's a 401 unauthorized error + if ( + err && + typeof err === "object" && + "status" in err && + err.status === 401 + ) { + router.push("/auth/signin"); + return; + } + const errorMessage = isInitialLoad ? `Failed to load ${errorMessagePrefix}. Please try refreshing the page.` : `Failed to load more ${errorMessagePrefix}. Please try again.`; @@ -107,7 +131,7 @@ export function usePagination({ })); } }, - [limit, model, order, fetchFunction, errorMessagePrefix], + [limit, model, order, fetchFunction, errorMessagePrefix, client, router], ); /** @@ -120,17 +144,28 @@ export function usePagination({ } }, [fetchData]); - // Auto-load initial data on mount + // Auto-load initial data on mount when enabled useEffect(() => { - if (!hasFetchedInitialData.current) { + // If using auth, wait for session to load + const isAuthReady = !useAuth || sessionStatus !== "loading"; + const shouldFetch = enabled && isAuthReady; + + if (shouldFetch && !hasFetchedInitialData.current) { hasFetchedInitialData.current = true; fetchData(); + } else if (!shouldFetch) { + // Reset the flag when disabled so it can fetch when re-enabled + hasFetchedInitialData.current = false; } - }, [fetchData]); + }, [fetchData, enabled, useAuth, sessionStatus]); + + // Override status if we're waiting for auth + const effectiveStatus = + useAuth && sessionStatus === "loading" ? "loading" : state.status; return { data: state.data, - status: state.status, + status: effectiveStatus, hasMore: state.hasMore, error: state.error, loadMore, diff --git a/llama_stack/ui/instrumentation.ts b/llama_stack/ui/instrumentation.ts new file mode 100644 index 000000000..903528443 --- /dev/null +++ b/llama_stack/ui/instrumentation.ts @@ -0,0 +1,11 @@ +/** + * Next.js Instrumentation + * This file is used for initializing monitoring, tracing, or other observability tools. + * It runs once when the server starts, before any application code. + * + * Learn more: https://nextjs.org/docs/app/building-your-application/optimizing/instrumentation + */ + +export async function register() { + await import("./lib/config-validator"); +} diff --git a/llama_stack/ui/lib/auth.ts b/llama_stack/ui/lib/auth.ts new file mode 100644 index 000000000..bc18b168b --- /dev/null +++ b/llama_stack/ui/lib/auth.ts @@ -0,0 +1,38 @@ +import { NextAuthOptions } from "next-auth"; +import GithubProvider from "next-auth/providers/github"; + +export const authOptions: NextAuthOptions = { + providers: [ + GithubProvider({ + clientId: process.env.GITHUB_CLIENT_ID!, + clientSecret: process.env.GITHUB_CLIENT_SECRET!, + authorization: { + params: { + scope: "read:user user:email", + }, + }, + }), + ], + debug: process.env.NODE_ENV === "development", + callbacks: { + async jwt({ token, account }) { + // Persist the OAuth access_token to the token right after signin + if (account) { + token.accessToken = account.access_token; + } + return token; + }, + async session({ session, token }) { + // Send properties to the client, like an access_token from a provider. + session.accessToken = token.accessToken as string; + return session; + }, + }, + pages: { + signIn: "/auth/signin", + error: "/auth/signin", // Redirect errors to our custom page + }, + session: { + strategy: "jwt", + }, +}; diff --git a/llama_stack/ui/lib/client.ts b/llama_stack/ui/lib/client.ts deleted file mode 100644 index 8492496e2..000000000 --- a/llama_stack/ui/lib/client.ts +++ /dev/null @@ -1,6 +0,0 @@ -import LlamaStackClient from "llama-stack-client"; - -export const client = new LlamaStackClient({ - baseURL: - typeof window !== "undefined" ? `${window.location.origin}/api` : "/api", -}); diff --git a/llama_stack/ui/lib/config-validator.ts b/llama_stack/ui/lib/config-validator.ts new file mode 100644 index 000000000..19f4397b8 --- /dev/null +++ b/llama_stack/ui/lib/config-validator.ts @@ -0,0 +1,56 @@ +/** + * Validates environment configuration for the application + * This is called during server initialization + */ +export function validateServerConfig() { + if (process.env.NODE_ENV === "development") { + console.log("šŸš€ Starting Llama Stack UI Server..."); + + // Check optional configurations + const optionalConfigs = { + NEXTAUTH_URL: process.env.NEXTAUTH_URL || "http://localhost:8322", + LLAMA_STACK_BACKEND_URL: + process.env.LLAMA_STACK_BACKEND_URL || "http://localhost:8321", + LLAMA_STACK_UI_PORT: process.env.LLAMA_STACK_UI_PORT || "8322", + GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID, + GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET, + }; + + console.log("\nšŸ“‹ Configuration:"); + console.log(` - NextAuth URL: ${optionalConfigs.NEXTAUTH_URL}`); + console.log(` - Backend URL: ${optionalConfigs.LLAMA_STACK_BACKEND_URL}`); + console.log(` - UI Port: ${optionalConfigs.LLAMA_STACK_UI_PORT}`); + + // Check GitHub OAuth configuration + if ( + !optionalConfigs.GITHUB_CLIENT_ID || + !optionalConfigs.GITHUB_CLIENT_SECRET + ) { + console.log( + "\nšŸ“ GitHub OAuth not configured (authentication features disabled)", + ); + console.log(" To enable GitHub OAuth:"); + console.log(" 1. Go to https://github.com/settings/applications/new"); + console.log( + " 2. Set Application name: Llama Stack UI (or your preferred name)", + ); + console.log(" 3. Set Homepage URL: http://localhost:8322"); + console.log( + " 4. Set Authorization callback URL: http://localhost:8322/api/auth/callback/github", + ); + console.log( + " 5. Create the app and copy the Client ID and Client Secret", + ); + console.log(" 6. Add them to your .env.local file:"); + console.log(" GITHUB_CLIENT_ID=your_client_id"); + console.log(" GITHUB_CLIENT_SECRET=your_client_secret"); + } else { + console.log(" - GitHub OAuth: āœ… Configured"); + } + + console.log(""); + } +} + +// Call this function when the module is imported +validateServerConfig(); diff --git a/llama_stack/ui/package-lock.json b/llama_stack/ui/package-lock.json index 4c4620ac2..8fd5fb56c 100644 --- a/llama_stack/ui/package-lock.json +++ b/llama_stack/ui/package-lock.json @@ -18,6 +18,7 @@ "llama-stack-client": "0.2.13", "lucide-react": "^0.510.0", "next": "15.3.3", + "next-auth": "^4.24.11", "next-themes": "^0.4.6", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -548,7 +549,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2423,6 +2423,15 @@ "node": ">=12.4.0" } }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/@pkgr/core": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.4.tgz", @@ -5279,7 +5288,6 @@ "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -9036,6 +9044,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9949,6 +9966,38 @@ } } }, + "node_modules/next-auth": { + "version": "4.24.11", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.11.tgz", + "integrity": "sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==", + "license": "ISC", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@panva/hkdf": "^1.0.2", + "cookie": "^0.7.0", + "jose": "^4.15.5", + "oauth": "^0.9.15", + "openid-client": "^5.4.0", + "preact": "^10.6.3", + "preact-render-to-string": "^5.1.19", + "uuid": "^8.3.2" + }, + "peerDependencies": { + "@auth/core": "0.34.2", + "next": "^12.2.5 || ^13 || ^14 || ^15", + "nodemailer": "^6.6.5", + "react": "^17.0.2 || ^18 || ^19", + "react-dom": "^17.0.2 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@auth/core": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/next-themes": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", @@ -10071,6 +10120,12 @@ "dev": true, "license": "MIT" }, + "node_modules/oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -10081,6 +10136,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -10194,6 +10258,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oidc-token-hash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz", + "integrity": "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==", + "license": "MIT", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -10233,6 +10306,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openid-client": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", + "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==", + "license": "MIT", + "dependencies": { + "jose": "^4.15.9", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openid-client/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/openid-client/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -10560,6 +10666,34 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/preact": { + "version": "10.26.9", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.9.tgz", + "integrity": "sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz", + "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==", + "license": "MIT", + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, + "node_modules/preact-render-to-string/node_modules/pretty-format": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", + "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", + "license": "MIT" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -12409,6 +12543,15 @@ } } }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/llama_stack/ui/package.json b/llama_stack/ui/package.json index 43a5c2ac1..9524ce0a5 100644 --- a/llama_stack/ui/package.json +++ b/llama_stack/ui/package.json @@ -23,6 +23,7 @@ "llama-stack-client": "0.2.13", "lucide-react": "^0.510.0", "next": "15.3.3", + "next-auth": "^4.24.11", "next-themes": "^0.4.6", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/llama_stack/ui/types/next-auth.d.ts b/llama_stack/ui/types/next-auth.d.ts new file mode 100644 index 000000000..4ed718132 --- /dev/null +++ b/llama_stack/ui/types/next-auth.d.ts @@ -0,0 +1,7 @@ +import NextAuth from "next-auth"; + +declare module "next-auth" { + interface Session { + accessToken?: string; + } +}