feat(auth,ui): support github sign-in in the UI (#2545)

# What does this PR do?
Uses NextAuth to add github sign in support.

## Test Plan
Start server with auth configured as in
https://github.com/meta-llama/llama-stack/pull/2509


https://github.com/user-attachments/assets/61ff7442-f601-4b39-8686-5d0afb3b45ac
This commit is contained in:
ehhuang 2025-07-08 11:02:57 -07:00 committed by GitHub
parent c8bac888af
commit daf660c4ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 577 additions and 81 deletions

View file

@ -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 };

View file

@ -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 (
<div className="flex items-center justify-center min-h-screen">
<div className="text-muted-foreground">Loading...</div>
</div>
);
}
return (
<div className="flex items-center justify-center min-h-screen">
<Card className="w-[400px]">
<CardHeader>
<CardTitle>Authentication</CardTitle>
<CardDescription>
{session
? "You are successfully authenticated!"
: "Sign in with GitHub to use your access token as an API key"}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{!session ? (
<Button
onClick={() => {
console.log("Signing in with GitHub...");
signIn("github", { callbackUrl: "/auth/signin" }).catch(
(error) => {
console.error("Sign in error:", error);
},
);
}}
className="w-full"
variant="default"
>
<Github className="mr-2 h-4 w-4" />
Sign in with GitHub
</Button>
) : (
<div className="space-y-4">
<div className="text-sm text-muted-foreground">
Signed in as {session.user?.email}
</div>
{session.accessToken && (
<div className="space-y-2">
<div className="text-sm font-medium">
GitHub Access Token:
</div>
<div className="flex gap-2">
<code className="flex-1 p-2 bg-muted rounded text-xs break-all">
{session.accessToken}
</code>
<Button
size="sm"
variant="outline"
onClick={handleCopyToken}
>
{copied ? (
<Check className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
</div>
<div className="text-xs text-muted-foreground">
This GitHub token will be used as your API key for
authenticated Llama Stack requests.
</div>
</div>
)}
<div className="flex gap-2">
<Button onClick={() => router.push("/")} className="flex-1">
<Home className="mr-2 h-4 w-4" />
Go to Dashboard
</Button>
<Button
onClick={() => signOut()}
variant="outline"
className="flex-1"
>
Sign out
</Button>
</div>
</div>
)}
</CardContent>
</Card>
</div>
);
}

View file

@ -1,5 +1,6 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { ThemeProvider } from "@/components/ui/theme-provider"; import { ThemeProvider } from "@/components/ui/theme-provider";
import { SessionProvider } from "@/components/providers/session-provider";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import { ModeToggle } from "@/components/ui/mode-toggle"; import { ModeToggle } from "@/components/ui/mode-toggle";
import "./globals.css"; import "./globals.css";
@ -21,11 +22,13 @@ export const metadata: Metadata = {
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"; import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
import { AppSidebar } from "@/components/layout/app-sidebar"; import { AppSidebar } from "@/components/layout/app-sidebar";
import { SignInButton } from "@/components/ui/sign-in-button";
export default function Layout({ children }: { children: React.ReactNode }) { export default function Layout({ children }: { children: React.ReactNode }) {
return ( return (
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<body className={`${geistSans.variable} ${geistMono.variable} font-sans`}> <body className={`${geistSans.variable} ${geistMono.variable} font-sans`}>
<SessionProvider>
<ThemeProvider <ThemeProvider
attribute="class" attribute="class"
defaultTheme="system" defaultTheme="system"
@ -41,7 +44,8 @@ export default function Layout({ children }: { children: React.ReactNode }) {
<SidebarTrigger /> <SidebarTrigger />
</div> </div>
<div className="flex-1 text-center"></div> <div className="flex-1 text-center"></div>
<div className="flex-none"> <div className="flex-none flex items-center gap-2">
<SignInButton />
<ModeToggle /> <ModeToggle />
</div> </div>
</div> </div>
@ -49,6 +53,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
</main> </main>
</SidebarProvider> </SidebarProvider>
</ThemeProvider> </ThemeProvider>
</SessionProvider>
</body> </body>
</html> </html>
); );

View file

@ -4,11 +4,12 @@ import { useEffect, useState } from "react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { ChatCompletion } from "@/lib/types"; import { ChatCompletion } from "@/lib/types";
import { ChatCompletionDetailView } from "@/components/chat-completions/chat-completion-detail"; 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() { export default function ChatCompletionDetailPage() {
const params = useParams(); const params = useParams();
const id = params.id as string; const id = params.id as string;
const client = useAuthClient();
const [completionDetail, setCompletionDetail] = const [completionDetail, setCompletionDetail] =
useState<ChatCompletion | null>(null); useState<ChatCompletion | null>(null);
@ -45,7 +46,7 @@ export default function ChatCompletionDetailPage() {
}; };
fetchCompletionDetail(); fetchCompletionDetail();
}, [id]); }, [id, client]);
return ( return (
<ChatCompletionDetailView <ChatCompletionDetailView

View file

@ -5,11 +5,12 @@ import { useParams } from "next/navigation";
import type { ResponseObject } from "llama-stack-client/resources/responses/responses"; import type { ResponseObject } from "llama-stack-client/resources/responses/responses";
import { OpenAIResponse, InputItemListResponse } from "@/lib/types"; import { OpenAIResponse, InputItemListResponse } from "@/lib/types";
import { ResponseDetailView } from "@/components/responses/responses-detail"; import { ResponseDetailView } from "@/components/responses/responses-detail";
import { client } from "@/lib/client"; import { useAuthClient } from "@/hooks/use-auth-client";
export default function ResponseDetailPage() { export default function ResponseDetailPage() {
const params = useParams(); const params = useParams();
const id = params.id as string; const id = params.id as string;
const client = useAuthClient();
const [responseDetail, setResponseDetail] = useState<OpenAIResponse | null>( const [responseDetail, setResponseDetail] = useState<OpenAIResponse | null>(
null, null,
@ -109,7 +110,7 @@ export default function ResponseDetailPage() {
}; };
fetchResponseDetail(); fetchResponseDetail();
}, [id]); }, [id, client]);
return ( return (
<ResponseDetailView <ResponseDetailView

View file

@ -12,24 +12,34 @@ jest.mock("next/navigation", () => ({
}), }),
})); }));
// Mock next-auth
jest.mock("next-auth/react", () => ({
useSession: () => ({
status: "authenticated",
data: { accessToken: "mock-token" },
}),
}));
// Mock helper functions // Mock helper functions
jest.mock("@/lib/truncate-text"); jest.mock("@/lib/truncate-text");
jest.mock("@/lib/format-message-content"); jest.mock("@/lib/format-message-content");
// Mock the client // Mock the auth client hook
jest.mock("@/lib/client", () => ({ const mockClient = {
client: {
chat: { chat: {
completions: { completions: {
list: jest.fn(), list: jest.fn(),
}, },
}, },
}, };
jest.mock("@/hooks/use-auth-client", () => ({
useAuthClient: () => mockClient,
})); }));
// Mock the usePagination hook // Mock the usePagination hook
const mockLoadMore = jest.fn(); const mockLoadMore = jest.fn();
jest.mock("@/hooks/usePagination", () => ({ jest.mock("@/hooks/use-pagination", () => ({
usePagination: jest.fn(() => ({ usePagination: jest.fn(() => ({
data: [], data: [],
status: "idle", status: "idle",
@ -47,7 +57,7 @@ import {
} from "@/lib/format-message-content"; } from "@/lib/format-message-content";
// Import the mocked hook // Import the mocked hook
import { usePagination } from "@/hooks/usePagination"; import { usePagination } from "@/hooks/use-pagination";
const mockedUsePagination = usePagination as jest.MockedFunction< const mockedUsePagination = usePagination as jest.MockedFunction<
typeof usePagination typeof usePagination
>; >;

View file

@ -10,8 +10,7 @@ import {
extractTextFromContentPart, extractTextFromContentPart,
extractDisplayableText, extractDisplayableText,
} from "@/lib/format-message-content"; } from "@/lib/format-message-content";
import { usePagination } from "@/hooks/usePagination"; import { usePagination } from "@/hooks/use-pagination";
import { client } from "@/lib/client";
interface ChatCompletionsTableProps { interface ChatCompletionsTableProps {
/** Optional pagination configuration */ /** Optional pagination configuration */
@ -32,12 +31,15 @@ function formatChatCompletionToRow(completion: ChatCompletion): LogTableRow {
export function ChatCompletionsTable({ export function ChatCompletionsTable({
paginationOptions, paginationOptions,
}: ChatCompletionsTableProps) { }: ChatCompletionsTableProps) {
const fetchFunction = async (params: { const fetchFunction = async (
client: ReturnType<typeof import("@/hooks/use-auth-client").useAuthClient>,
params: {
after?: string; after?: string;
limit: number; limit: number;
model?: string; model?: string;
order?: string; order?: string;
}) => { },
) => {
const response = await client.chat.completions.list({ const response = await client.chat.completions.list({
after: params.after, after: params.after,
limit: params.limit, limit: params.limit,

View file

@ -12,7 +12,7 @@ jest.mock("next/navigation", () => ({
})); }));
// Mock the useInfiniteScroll hook // Mock the useInfiniteScroll hook
jest.mock("@/hooks/useInfiniteScroll", () => ({ jest.mock("@/hooks/use-infinite-scroll", () => ({
useInfiniteScroll: jest.fn((onLoadMore, options) => { useInfiniteScroll: jest.fn((onLoadMore, options) => {
const ref = React.useRef(null); const ref = React.useRef(null);

View file

@ -4,7 +4,7 @@ import { useRouter } from "next/navigation";
import { useRef } from "react"; import { useRef } from "react";
import { truncateText } from "@/lib/truncate-text"; import { truncateText } from "@/lib/truncate-text";
import { PaginationStatus } from "@/lib/types"; import { PaginationStatus } from "@/lib/types";
import { useInfiniteScroll } from "@/hooks/useInfiniteScroll"; import { useInfiniteScroll } from "@/hooks/use-infinite-scroll";
import { import {
Table, Table,
TableBody, TableBody,

View file

@ -0,0 +1,7 @@
"use client";
import { SessionProvider as NextAuthSessionProvider } from "next-auth/react";
export function SessionProvider({ children }: { children: React.ReactNode }) {
return <NextAuthSessionProvider>{children}</NextAuthSessionProvider>;
}

View file

@ -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 // Mock helper functions
jest.mock("@/lib/truncate-text"); jest.mock("@/lib/truncate-text");
// Mock the client // Mock the auth client hook
jest.mock("@/lib/client", () => ({ const mockClient = {
client: {
responses: { responses: {
list: jest.fn(), list: jest.fn(),
}, },
}, };
jest.mock("@/hooks/use-auth-client", () => ({
useAuthClient: () => mockClient,
})); }));
// Mock the usePagination hook // Mock the usePagination hook
const mockLoadMore = jest.fn(); const mockLoadMore = jest.fn();
jest.mock("@/hooks/usePagination", () => ({ jest.mock("@/hooks/use-pagination", () => ({
usePagination: jest.fn(() => ({ usePagination: jest.fn(() => ({
data: [], data: [],
status: "idle", status: "idle",
@ -40,7 +50,7 @@ jest.mock("@/hooks/usePagination", () => ({
import { truncateText as originalTruncateText } from "@/lib/truncate-text"; import { truncateText as originalTruncateText } from "@/lib/truncate-text";
// Import the mocked hook // Import the mocked hook
import { usePagination } from "@/hooks/usePagination"; import { usePagination } from "@/hooks/use-pagination";
const mockedUsePagination = usePagination as jest.MockedFunction< const mockedUsePagination = usePagination as jest.MockedFunction<
typeof usePagination typeof usePagination
>; >;

View file

@ -6,8 +6,7 @@ import {
UsePaginationOptions, UsePaginationOptions,
} from "@/lib/types"; } from "@/lib/types";
import { LogsTable, LogTableRow } from "@/components/logs/logs-table"; import { LogsTable, LogTableRow } from "@/components/logs/logs-table";
import { usePagination } from "@/hooks/usePagination"; import { usePagination } from "@/hooks/use-pagination";
import { client } from "@/lib/client";
import type { ResponseListResponse } from "llama-stack-client/resources/responses/responses"; import type { ResponseListResponse } from "llama-stack-client/resources/responses/responses";
import { import {
isMessageInput, isMessageInput,
@ -125,12 +124,15 @@ function formatResponseToRow(response: OpenAIResponse): LogTableRow {
} }
export function ResponsesTable({ paginationOptions }: ResponsesTableProps) { export function ResponsesTable({ paginationOptions }: ResponsesTableProps) {
const fetchFunction = async (params: { const fetchFunction = async (
client: ReturnType<typeof import("@/hooks/use-auth-client").useAuthClient>,
params: {
after?: string; after?: string;
limit: number; limit: number;
model?: string; model?: string;
order?: string; order?: string;
}) => { },
) => {
const response = await client.responses.list({ const response = await client.responses.list({
after: params.after, after: params.after,
limit: params.limit, limit: params.limit,

View file

@ -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 (
<Button variant="ghost" size="sm" asChild>
<Link href="/auth/signin" className="flex items-center">
<User className="mr-2 h-4 w-4" />
<span>
{status === "loading"
? "Loading..."
: session
? session.user?.email || "Signed In"
: "Sign In"}
</span>
</Link>
</Button>
);
}

View file

@ -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;
}

View file

@ -2,6 +2,9 @@
import { useState, useCallback, useEffect, useRef } from "react"; import { useState, useCallback, useEffect, useRef } from "react";
import { PaginationStatus, UsePaginationOptions } from "@/lib/types"; 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<T> { interface PaginationState<T> {
data: T[]; data: T[];
@ -28,13 +31,18 @@ export interface PaginationReturn<T> {
} }
interface UsePaginationParams<T> extends UsePaginationOptions { interface UsePaginationParams<T> extends UsePaginationOptions {
fetchFunction: (params: { fetchFunction: (
client: ReturnType<typeof useAuthClient>,
params: {
after?: string; after?: string;
limit: number; limit: number;
model?: string; model?: string;
order?: string; order?: string;
}) => Promise<PaginationResponse<T>>; },
) => Promise<PaginationResponse<T>>;
errorMessagePrefix: string; errorMessagePrefix: string;
enabled?: boolean;
useAuth?: boolean;
} }
export function usePagination<T>({ export function usePagination<T>({
@ -43,7 +51,12 @@ export function usePagination<T>({
order = "desc", order = "desc",
fetchFunction, fetchFunction,
errorMessagePrefix, errorMessagePrefix,
enabled = true,
useAuth = true,
}: UsePaginationParams<T>): PaginationReturn<T> { }: UsePaginationParams<T>): PaginationReturn<T> {
const { status: sessionStatus } = useSession();
const client = useAuthClient();
const router = useRouter();
const [state, setState] = useState<PaginationState<T>>({ const [state, setState] = useState<PaginationState<T>>({
data: [], data: [],
status: "loading", status: "loading",
@ -74,7 +87,7 @@ export function usePagination<T>({
error: null, error: null,
})); }));
const response = await fetchFunction({ const response = await fetchFunction(client, {
after: after || undefined, after: after || undefined,
limit: fetchLimit, limit: fetchLimit,
...(model && { model }), ...(model && { model }),
@ -91,6 +104,17 @@ export function usePagination<T>({
status: "idle", status: "idle",
})); }));
} catch (err) { } 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 const errorMessage = isInitialLoad
? `Failed to load ${errorMessagePrefix}. Please try refreshing the page.` ? `Failed to load ${errorMessagePrefix}. Please try refreshing the page.`
: `Failed to load more ${errorMessagePrefix}. Please try again.`; : `Failed to load more ${errorMessagePrefix}. Please try again.`;
@ -107,7 +131,7 @@ export function usePagination<T>({
})); }));
} }
}, },
[limit, model, order, fetchFunction, errorMessagePrefix], [limit, model, order, fetchFunction, errorMessagePrefix, client, router],
); );
/** /**
@ -120,17 +144,28 @@ export function usePagination<T>({
} }
}, [fetchData]); }, [fetchData]);
// Auto-load initial data on mount // Auto-load initial data on mount when enabled
useEffect(() => { 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; hasFetchedInitialData.current = true;
fetchData(); 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 { return {
data: state.data, data: state.data,
status: state.status, status: effectiveStatus,
hasMore: state.hasMore, hasMore: state.hasMore,
error: state.error, error: state.error,
loadMore, loadMore,

View file

@ -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");
}

View file

@ -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",
},
};

View file

@ -1,6 +0,0 @@
import LlamaStackClient from "llama-stack-client";
export const client = new LlamaStackClient({
baseURL:
typeof window !== "undefined" ? `${window.location.origin}/api` : "/api",
});

View file

@ -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();

View file

@ -18,6 +18,7 @@
"llama-stack-client": "0.2.13", "llama-stack-client": "0.2.13",
"lucide-react": "^0.510.0", "lucide-react": "^0.510.0",
"next": "15.3.3", "next": "15.3.3",
"next-auth": "^4.24.11",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
@ -548,7 +549,6 @@
"version": "7.27.1", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz",
"integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@ -2423,6 +2423,15 @@
"node": ">=12.4.0" "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": { "node_modules/@pkgr/core": {
"version": "0.2.4", "version": "0.2.4",
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.4.tgz", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.4.tgz",
@ -5279,7 +5288,6 @@
"version": "0.7.2", "version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.6" "node": ">= 0.6"
@ -9036,6 +9044,15 @@
"jiti": "lib/jiti-cli.mjs" "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": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "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": { "node_modules/next-themes": {
"version": "0.4.6", "version": "0.4.6",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
@ -10071,6 +10120,12 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -10081,6 +10136,15 @@
"node": ">=0.10.0" "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": { "node_modules/object-inspect": {
"version": "1.13.4", "version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@ -10194,6 +10258,15 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/on-finished": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@ -10233,6 +10306,39 @@
"url": "https://github.com/sponsors/sindresorhus" "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": { "node_modules/optionator": {
"version": "0.9.4", "version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -10560,6 +10666,34 @@
"node": "^10 || ^12 || >=14" "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": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "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": { "node_modules/v8-compile-cache-lib": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",

View file

@ -23,6 +23,7 @@
"llama-stack-client": "0.2.13", "llama-stack-client": "0.2.13",
"lucide-react": "^0.510.0", "lucide-react": "^0.510.0",
"next": "15.3.3", "next": "15.3.3",
"next-auth": "^4.24.11",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",

7
llama_stack/ui/types/next-auth.d.ts vendored Normal file
View file

@ -0,0 +1,7 @@
import NextAuth from "next-auth";
declare module "next-auth" {
interface Session {
accessToken?: string;
}
}