feat: implement GitHub OIDC authentication

- Add GitHub OAuth 2.0 authentication flow
- Create authentication routes: /auth/login, /auth/callback, /auth/logout
- Implement OAuth server utilities with proper session management
- Add authentication hooks and client-side state management
- Update header with login/logout controls and user menu
- Create user profile page with GitHub integration
- Add environment variables for GitHub OAuth configuration
- Include comprehensive documentation for setup and usage
- Enhance profile store with authentication state
- Add authentication status API endpoint

Closes: GitHub authentication implementation
This commit is contained in:
Nirmal Arya
2025-05-31 15:40:32 -04:00
parent e0eb402a85
commit 1687d812bf
12 changed files with 1961 additions and 17 deletions

View File

@@ -1,5 +1,24 @@
# Rename this file to .env once you have filled in the below environment variables!
# GitHub OAuth Authentication Configuration
# ---------------------------------------
# To set up GitHub OAuth:
# 1. Go to https://github.com/settings/developers
# 2. Click "New OAuth App"
# 3. Fill in the application details:
# - Application name: Buildify (or your preferred name)
# - Homepage URL: http://localhost:5173 (or your deployment URL)
# - Authorization callback URL: http://localhost:5173/auth/callback
# 4. Click "Register application"
# 5. Copy the Client ID and generate a new Client Secret
# 6. Paste them below
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
# Session Secret - Used to encrypt session cookies
# Generate a random string (e.g., using `openssl rand -hex 32` in terminal)
SESSION_SECRET=
# Get your GROQ API Key here -
# https://console.groq.com/keys
# You only need this environment variable set if you want to use Groq models

View File

@@ -4,10 +4,12 @@ import { chatStore } from '~/lib/stores/chat';
import { classNames } from '~/utils/classNames';
import { HeaderActionButtons } from './HeaderActionButtons.client';
import { ChatDescription } from '~/lib/persistence/ChatDescription.client';
import { useAuth } from '~/lib/hooks/useAuth';
import { Link } from '@remix-run/react';
export function Header() {
const chat = useStore(chatStore);
return (
<header
className={classNames('flex items-center p-5 border-b h-[var(--header-height)]', {
@@ -23,20 +25,95 @@ export function Header() {
<img src="/logo-dark-styled.png" alt="logo" className="w-[90px] inline-block hidden dark:block" />
</a>
</div>
{chat.started && ( // Display ChatDescription and HeaderActionButtons only when the chat has started.
<>
<span className="flex-1 px-4 truncate text-center text-bolt-elements-textPrimary">
<ClientOnly>{() => <ChatDescription />}</ClientOnly>
</span>
{chat.started && (
<span className="flex-1 px-4 truncate text-center text-bolt-elements-textPrimary">
<ClientOnly>{() => <ChatDescription />}</ClientOnly>
</span>
)}
<div className="flex items-center ml-auto">
{/* Authentication Controls - Always visible */}
<ClientOnly>
{() => <AuthControls />}
</ClientOnly>
{/* Existing Action Buttons - Only when chat has started */}
{chat.started && (
<ClientOnly>
{() => (
<div className="mr-1">
<div className="ml-2">
<HeaderActionButtons />
</div>
)}
</ClientOnly>
</>
)}
)}
</div>
</header>
);
}
function AuthControls() {
const { isAuthenticated, isLoading, user, login, logout } = useAuth();
if (isLoading) {
return (
<div className="flex items-center h-8 px-2 text-bolt-elements-textTertiary">
<div className="i-svg-spinners:270-ring-with-bg w-5 h-5" />
</div>
);
}
if (isAuthenticated && user) {
return (
<div className="flex items-center">
<div className="relative group">
<button
className="flex items-center gap-2 px-2 py-1 rounded-md hover:bg-bolt-elements-item-backgroundActive transition-colors"
aria-label="User menu"
>
<img
src={user.avatar}
alt={`${user.username}'s avatar`}
className="w-8 h-8 rounded-full border border-bolt-elements-borderColor"
/>
<span className="text-sm font-medium text-bolt-elements-textPrimary hidden sm:block">
{user.username}
</span>
<div className="i-ph:caret-down w-4 h-4 text-bolt-elements-textTertiary" />
</button>
<div className="absolute right-0 mt-1 w-48 py-1 bg-bolt-elements-background-depth-2 rounded-md shadow-lg border border-bolt-elements-borderColor hidden group-hover:block z-50">
<div className="px-4 py-2 text-sm text-bolt-elements-textSecondary border-b border-bolt-elements-borderColor">
Signed in as <span className="font-semibold text-bolt-elements-textPrimary">{user.username}</span>
</div>
<Link
to="/profile"
className="block px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive"
>
Your profile
</Link>
<button
onClick={() => logout()}
className="block w-full text-left px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive"
>
Sign out
</button>
</div>
</div>
</div>
);
}
return (
<button
onClick={() => login()}
className="flex items-center gap-2 px-3 py-1.5 rounded-md bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent hover:bg-bolt-elements-item-backgroundAccentHover transition-colors text-sm font-medium"
>
<div className="i-ph:sign-in w-4 h-4" />
Sign in
</button>
);
}

View File

@@ -0,0 +1,241 @@
import { OAuthApp } from '@octokit/oauth-app';
import { createCookieSessionStorage, redirect } from '@remix-run/node';
import { v4 as uuidv4 } from 'uuid';
// Define types for GitHub user data
export interface GitHubUser {
id: number;
login: string;
name: string | null;
email: string | null;
avatar_url: string;
html_url: string;
}
// GitHub API response types
interface GitHubUserResponse {
id: number;
login: string;
name: string | null;
email: string | null;
avatar_url: string;
html_url: string;
[key: string]: any; // For other properties we don't explicitly use
}
interface GitHubEmailResponse {
email: string;
primary: boolean;
verified: boolean;
visibility: string | null;
}
// Define types for session data
export interface AuthSession {
accessToken: string;
user: GitHubUser;
}
// Error class for authentication errors
export class AuthError extends Error {
constructor(message: string) {
super(message);
this.name = 'AuthError';
}
}
// Get environment variables
const clientId = process.env.GITHUB_CLIENT_ID;
const clientSecret = process.env.GITHUB_CLIENT_SECRET;
const sessionSecret = process.env.SESSION_SECRET || 'buildify-session-secret';
// Validate required environment variables
if (!clientId || !clientSecret) {
console.warn(
'GitHub OAuth is not configured properly. Please set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET environment variables.'
);
}
// Create OAuth app instance
export const oauthApp = new OAuthApp({
clientId: clientId || '',
clientSecret: clientSecret || '',
defaultScopes: ['read:user', 'user:email'],
});
// Create session storage
const sessionStorage = createCookieSessionStorage({
cookie: {
name: 'buildify_auth_session',
httpOnly: true,
path: '/',
sameSite: 'lax',
secrets: [sessionSecret],
secure: process.env.NODE_ENV === 'production',
},
});
// Get session from request
export async function getSession(request: Request) {
const cookie = request.headers.get('Cookie');
return sessionStorage.getSession(cookie);
}
// Generate state parameter for OAuth flow to prevent CSRF attacks
export function generateState() {
return uuidv4();
}
// Store state in session
export async function storeState(request: Request, state: string) {
const session = await getSession(request);
session.set('oauth:state', state);
return sessionStorage.commitSession(session);
}
// Verify state from session
export async function verifyState(request: Request, state: string) {
const session = await getSession(request);
const storedState = session.get('oauth:state');
if (!storedState || storedState !== state) {
throw new AuthError('Invalid state parameter. Possible CSRF attack.');
}
// Clear the state after verification
session.unset('oauth:state');
return sessionStorage.commitSession(session);
}
// Generate authorization URL
export function getAuthorizationUrl(state: string, redirectUri?: string) {
return oauthApp.getWebFlowAuthorizationUrl({
state,
redirectUrl: redirectUri,
});
}
// Exchange code for token
export async function exchangeCodeForToken(code: string, state: string) {
try {
const { authentication } = await oauthApp.createToken({
code,
state,
});
return {
accessToken: authentication.token,
};
} catch (error) {
console.error('Error exchanging code for token:', error);
throw new AuthError('Failed to exchange code for token');
}
}
// Fetch user data from GitHub API
export async function fetchGitHubUser(accessToken: string): Promise<GitHubUser> {
try {
const response = await fetch('https://api.github.com/user', {
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: `token ${accessToken}`,
},
});
if (!response.ok) {
throw new Error(`GitHub API responded with ${response.status}`);
}
const userData = await response.json() as GitHubUserResponse;
// If email is not public, try to get primary email
let email = userData.email;
if (!email) {
const emailsResponse = await fetch('https://api.github.com/user/emails', {
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: `token ${accessToken}`,
},
});
if (emailsResponse.ok) {
const emails = await emailsResponse.json() as GitHubEmailResponse[];
const primaryEmail = emails.find((e) => e.primary);
email = primaryEmail?.email || null;
}
}
return {
id: userData.id,
login: userData.login,
name: userData.name,
email: email,
avatar_url: userData.avatar_url,
html_url: userData.html_url,
};
} catch (error) {
console.error('Error fetching GitHub user:', error);
throw new AuthError('Failed to fetch user data from GitHub');
}
}
// Create user session
export async function createUserSession(request: Request, authSession: AuthSession, redirectTo: string) {
const session = await getSession(request);
// Store user data in session
session.set('auth:user', authSession.user);
session.set('auth:accessToken', authSession.accessToken);
// Commit session and redirect
return redirect(redirectTo, {
headers: {
'Set-Cookie': await sessionStorage.commitSession(session, {
maxAge: 60 * 60 * 24 * 7, // 1 week
}),
},
});
}
// Get user from session
export async function getUserFromSession(request: Request): Promise<GitHubUser | null> {
const session = await getSession(request);
const user = session.get('auth:user');
return user || null;
}
// Get access token from session
export async function getAccessToken(request: Request): Promise<string | null> {
const session = await getSession(request);
const accessToken = session.get('auth:accessToken');
return accessToken || null;
}
// Check if user is authenticated
export async function isAuthenticated(request: Request): Promise<boolean> {
const user = await getUserFromSession(request);
return user !== null;
}
// Require authentication
export async function requireAuthentication(request: Request, redirectTo: string = '/login') {
const authenticated = await isAuthenticated(request);
if (!authenticated) {
const searchParams = new URLSearchParams([['redirectTo', request.url]]);
throw redirect(`${redirectTo}?${searchParams}`);
}
return await getUserFromSession(request);
}
// Logout - destroy session
export async function logout(request: Request, redirectTo: string = '/') {
const session = await getSession(request);
return redirect(redirectTo, {
headers: {
'Set-Cookie': await sessionStorage.destroySession(session),
},
});
}

134
app/lib/hooks/useAuth.ts Normal file
View File

@@ -0,0 +1,134 @@
import { useEffect, useState } from 'react';
import { useNavigate, useLocation } from '@remix-run/react';
import {
profileStore,
setAuthenticatedUser,
clearAuthState,
isAuthenticated as getIsAuthenticated,
getGitHubUser
} from '~/lib/stores/profile';
import { useStore } from '@nanostores/react';
import type { GitHubUser } from '~/lib/auth/github-oauth.server';
// Define the API response type for auth status
interface AuthStatusResponse {
isAuthenticated: boolean;
user?: GitHubUser;
tokenStatus?: {
hasToken: boolean;
};
error?: string;
}
/**
* Hook to manage authentication state and sync between server and client
*/
export function useAuth() {
const navigate = useNavigate();
const location = useLocation();
const profile = useStore(profileStore);
const [isLoading, setIsLoading] = useState(true);
const [serverChecked, setServerChecked] = useState(false);
// Sync server authentication state with client on initial load
useEffect(() => {
const checkServerAuth = async () => {
try {
setIsLoading(true);
// Fetch authentication status from server
const response = await fetch('/api/auth/status');
if (response.ok) {
const data = await response.json() as AuthStatusResponse;
if (data.isAuthenticated && data.user) {
// Update client state with server auth data
setAuthenticatedUser(data.user);
} else if (profile.isAuthenticated) {
// Clear client state if server says not authenticated
clearAuthState();
}
} else {
// Handle error - assume not authenticated
if (profile.isAuthenticated) {
clearAuthState();
}
}
} catch (error) {
console.error('Error checking authentication status:', error);
} finally {
setIsLoading(false);
setServerChecked(true);
}
};
// Only check server auth once
if (!serverChecked) {
checkServerAuth();
}
}, [serverChecked, profile.isAuthenticated]);
/**
* Navigate to login page
*/
const login = (redirectTo?: string) => {
const searchParams = new URLSearchParams();
if (redirectTo || location.pathname !== '/login') {
searchParams.set('redirectTo', redirectTo || location.pathname);
}
const searchParamsString = searchParams.toString();
navigate(`/auth/login${searchParamsString ? `?${searchParamsString}` : ''}`);
};
/**
* Navigate to logout page
*/
const logout = (redirectTo?: string) => {
const searchParams = new URLSearchParams();
if (redirectTo) {
searchParams.set('redirectTo', redirectTo);
}
const searchParamsString = searchParams.toString();
navigate(`/auth/logout${searchParamsString ? `?${searchParamsString}` : ''}`);
};
/**
* Check if user needs to authenticate for a protected resource
* and redirect to login if needed
*/
const requireAuth = (redirectIfNotAuth: boolean = true) => {
if (!isLoading && !profile.isAuthenticated && redirectIfNotAuth) {
login();
return false;
}
return profile.isAuthenticated;
};
return {
// Authentication state
isAuthenticated: profile.isAuthenticated,
isLoading,
// User data
user: profile.isAuthenticated ? {
username: profile.username,
avatar: profile.avatar,
bio: profile.bio,
githubUser: profile.github,
lastLogin: profile.lastLogin
} : null,
// GitHub specific data
githubUser: getGitHubUser(),
// Actions
login,
logout,
requireAuth,
};
}

View File

@@ -1,23 +1,54 @@
import { atom } from 'nanostores';
/**
* Enhanced Profile interface with authentication-related fields
*/
interface Profile {
// Basic profile fields (original)
username: string;
bio: string;
avatar: string;
// Authentication state
isAuthenticated: boolean;
lastLogin?: number; // timestamp
// GitHub user data
github?: {
id: number;
login: string;
name: string | null;
email: string | null;
avatar_url: string;
html_url: string;
};
}
// Initialize with stored profile or defaults
const storedProfile = typeof window !== 'undefined' ? localStorage.getItem('bolt_profile') : null;
const initialProfile: Profile = storedProfile
? JSON.parse(storedProfile)
? {
// Ensure backward compatibility with existing profile data
...{
username: '',
bio: '',
avatar: '',
isAuthenticated: false,
},
...JSON.parse(storedProfile),
}
: {
username: '',
bio: '',
avatar: '',
isAuthenticated: false,
};
export const profileStore = atom<Profile>(initialProfile);
/**
* Update profile with partial data
*/
export const updateProfile = (updates: Partial<Profile>) => {
profileStore.set({ ...profileStore.get(), ...updates });
@@ -26,3 +57,57 @@ export const updateProfile = (updates: Partial<Profile>) => {
localStorage.setItem('bolt_profile', JSON.stringify(profileStore.get()));
}
};
/**
* Set authenticated user data from GitHub
*/
export const setAuthenticatedUser = (githubUser: {
id: number;
login: string;
name: string | null;
email: string | null;
avatar_url: string;
html_url: string;
}) => {
const now = Date.now();
updateProfile({
username: githubUser.login,
avatar: githubUser.avatar_url,
// Keep existing bio if available
bio: profileStore.get().bio || githubUser.name || '',
isAuthenticated: true,
lastLogin: now,
github: githubUser,
});
};
/**
* Check if the user is authenticated
*/
export const isAuthenticated = (): boolean => {
return profileStore.get().isAuthenticated;
};
/**
* Clear authentication state
*/
export const clearAuthState = () => {
const currentProfile = profileStore.get();
updateProfile({
// Keep username/bio/avatar if user wants to
// but clear authentication state
isAuthenticated: false,
github: undefined,
lastLogin: undefined,
});
};
/**
* Get GitHub user data if authenticated
*/
export const getGitHubUser = () => {
const profile = profileStore.get();
return profile.isAuthenticated ? profile.github : null;
};

View File

@@ -0,0 +1,57 @@
import { json, type LoaderFunctionArgs } from '@remix-run/node';
import {
getUserFromSession,
isAuthenticated,
getAccessToken,
} from '~/lib/auth/github-oauth.server';
/**
* API route to check authentication status
* Used by the client to sync authentication state between server and client
*/
export async function loader({ request }: LoaderFunctionArgs) {
// Only allow GET requests
if (request.method !== 'GET') {
return json(
{ error: 'Method not allowed' },
{ status: 405, headers: { 'Allow': 'GET' } }
);
}
try {
// Check if user is authenticated
const authenticated = await isAuthenticated(request);
if (!authenticated) {
// Return unauthenticated status
return json({ isAuthenticated: false });
}
// Get user data from session
const user = await getUserFromSession(request);
// Get current access token
const accessToken = await getAccessToken(request);
// Build response
return json({
isAuthenticated: true,
user,
// Don't expose the actual token to the client
tokenStatus: {
hasToken: Boolean(accessToken),
},
});
} catch (error) {
console.error('Error checking authentication status:', error);
// Return error response
return json(
{
isAuthenticated: false,
error: 'Failed to check authentication status'
},
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,135 @@
import { json, redirect, type LoaderFunctionArgs, type MetaFunction } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
import {
AuthError,
createUserSession,
exchangeCodeForToken,
fetchGitHubUser,
verifyState,
} from '~/lib/auth/github-oauth.server';
import BackgroundRays from '~/components/ui/BackgroundRays';
export const meta: MetaFunction = () => {
return [
{ title: 'Authenticating...' },
{ name: 'description', content: 'Completing GitHub authentication' },
];
};
export async function loader({ request }: LoaderFunctionArgs) {
// Get URL parameters
const url = new URL(request.url);
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
const error = url.searchParams.get('error');
const errorDescription = url.searchParams.get('error_description');
// Get the redirect URL from state or default to home
const redirectTo = url.searchParams.get('redirectTo') || '/';
// Handle GitHub OAuth errors
if (error) {
return json({
success: false,
error: error,
errorDescription: errorDescription || 'An error occurred during authentication',
redirectTo: '/',
});
}
// Validate required parameters
if (!code || !state) {
return json({
success: false,
error: 'invalid_request',
errorDescription: 'Missing required parameters',
redirectTo: '/login',
});
}
try {
// Verify state parameter to prevent CSRF attacks
const cookieHeader = await verifyState(request, state);
// Exchange authorization code for access token
const { accessToken } = await exchangeCodeForToken(code, state);
// Fetch user data from GitHub API
const user = await fetchGitHubUser(accessToken);
// Create user session and redirect
return createUserSession(
request,
{
accessToken,
user,
},
redirectTo
);
} catch (error) {
console.error('Authentication error:', error);
let errorMessage = 'An unexpected error occurred during authentication';
let errorCode = 'server_error';
if (error instanceof AuthError) {
errorMessage = error.message;
errorCode = 'auth_error';
}
return json({
success: false,
error: errorCode,
errorDescription: errorMessage,
redirectTo: '/login',
});
}
}
export default function AuthCallback() {
const data = useLoaderData<typeof loader>();
// Only render the component if we have error data
// On successful auth, the server will redirect immediately
if (data && 'success' in data && !data.success) {
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-bolt-elements-background-depth-1">
<BackgroundRays />
<div className="w-full max-w-md p-8 space-y-8 bg-bolt-elements-background-depth-2 rounded-lg shadow-lg text-center">
<div className="text-red-500">
<svg xmlns="http://www.w3.org/2000/svg" className="h-16 w-16 mx-auto" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h1 className="text-2xl font-bold mt-4">Authentication Failed</h1>
<p className="mt-2 text-bolt-content-secondary">{data.errorDescription}</p>
</div>
<p className="text-sm text-bolt-content-tertiary">
Redirecting you back in a moment...
</p>
</div>
</div>
);
}
// Loading state
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-bolt-elements-background-depth-1">
<BackgroundRays />
<div className="w-full max-w-md p-8 space-y-8 bg-bolt-elements-background-depth-2 rounded-lg shadow-lg text-center">
<div className="text-bolt-content-primary">
<svg className="animate-spin h-16 w-16 mx-auto text-bolt-content-accent" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<h1 className="text-2xl font-bold mt-4">Authenticating...</h1>
<p className="mt-2 text-bolt-content-secondary">
Completing your sign-in with GitHub
</p>
</div>
</div>
</div>
);
}

162
app/routes/auth.login.tsx Normal file
View File

@@ -0,0 +1,162 @@
import { json, redirect, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from '@remix-run/node';
import { Form, useActionData, useLoaderData, useNavigation } from '@remix-run/react';
import { useEffect, useState } from 'react';
import { generateState, getAuthorizationUrl, isAuthenticated, storeState } from '~/lib/auth/github-oauth.server';
import BackgroundRays from '~/components/ui/BackgroundRays';
export const meta: MetaFunction = () => {
return [
{ title: 'Login to Buildify' },
{ name: 'description', content: 'Login to Buildify using your GitHub account' },
];
};
export async function loader({ request }: LoaderFunctionArgs) {
// Check if user is already authenticated
const authenticated = await isAuthenticated(request);
if (authenticated) {
// Redirect to home page if already logged in
return redirect('/');
}
// Get redirect URL from query params (if any)
const url = new URL(request.url);
const redirectTo = url.searchParams.get('redirectTo') || '/';
// Check if GitHub OAuth is configured
const clientId = process.env.GITHUB_CLIENT_ID;
const clientSecret = process.env.GITHUB_CLIENT_SECRET;
const isConfigured = Boolean(clientId && clientSecret);
return json({
redirectTo,
isConfigured,
error: isConfigured ? null : 'GitHub OAuth is not configured. Please set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET environment variables.',
});
}
export async function action({ request }: ActionFunctionArgs) {
// Check if GitHub OAuth is configured
const clientId = process.env.GITHUB_CLIENT_ID;
const clientSecret = process.env.GITHUB_CLIENT_SECRET;
if (!clientId || !clientSecret) {
return json(
{ error: 'GitHub OAuth is not configured. Please set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET environment variables.' },
{ status: 400 }
);
}
// Get form data
const formData = await request.formData();
const redirectTo = formData.get('redirectTo')?.toString() || '/';
try {
// Generate state for CSRF protection
const state = generateState();
// Get the callback URL
const url = new URL(request.url);
const callbackUrl = `${url.origin}/auth/callback`;
// Generate authorization URL
const { url: authorizationUrl } = getAuthorizationUrl(state, callbackUrl);
// Store state in session
const cookie = await storeState(request, state);
// Redirect to GitHub authorization URL
return redirect(authorizationUrl, {
headers: {
'Set-Cookie': cookie,
},
});
} catch (error) {
console.error('Error initiating GitHub OAuth flow:', error);
return json(
{ error: 'Failed to initiate login. Please try again later.' },
{ status: 500 }
);
}
}
export default function Login() {
const { redirectTo, isConfigured, error } = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
const [errorMessage, setErrorMessage] = useState<string | null>(null);
// Set error message from loader or action data
useEffect(() => {
setErrorMessage(error || actionData?.error || null);
}, [error, actionData]);
const isSubmitting = navigation.state === 'submitting';
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-bolt-elements-background-depth-1">
<BackgroundRays />
<div className="w-full max-w-md p-8 space-y-8 bg-bolt-elements-background-depth-2 rounded-lg shadow-lg">
<div className="text-center">
<h1 className="text-3xl font-bold text-bolt-content-primary">Login to Buildify</h1>
<p className="mt-2 text-bolt-content-secondary">
Connect with your GitHub account to get started
</p>
</div>
{errorMessage && (
<div className="p-4 text-sm text-red-500 bg-red-100 rounded-md">
{errorMessage}
</div>
)}
<Form method="post" className="space-y-6">
<input type="hidden" name="redirectTo" value={redirectTo} />
<button
type="submit"
disabled={!isConfigured || isSubmitting}
className={`
w-full flex items-center justify-center py-3 px-4 rounded-md
${isConfigured
? 'bg-[#2da44e] hover:bg-[#2c974b] text-white'
: 'bg-gray-300 text-gray-500 cursor-not-allowed'}
transition-colors duration-200 font-medium
`}
>
{isSubmitting ? (
<span className="flex items-center">
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Connecting...
</span>
) : (
<span className="flex items-center">
<svg className="w-5 h-5 mr-2" viewBox="0 0 24 24" fill="currentColor">
<path fillRule="evenodd" clipRule="evenodd" d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385c.6.105.825-.255.825-.57c0-.285-.015-1.23-.015-2.235c-3.015.555-3.795-.735-4.035-1.41c-.135-.345-.72-1.41-1.23-1.695c-.42-.225-1.02-.78-.015-.795c.945-.015 1.62.87 1.845 1.23c1.08 1.815 2.805 1.305 3.495.99c.105-.78.42-1.305.765-1.605c-2.67-.3-5.46-1.335-5.46-5.925c0-1.305.465-2.385 1.23-3.225c-.12-.3-.54-1.53.12-3.18c0 0 1.005-.315 3.3 1.23c.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23c.66 1.65.24 2.88.12 3.18c.765.84 1.23 1.905 1.23 3.225c0 4.605-2.805 5.625-5.475 5.925c.435.375.81 1.095.81 2.22c0 1.605-.015 2.895-.015 3.3c0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
</svg>
Continue with GitHub
</span>
)}
</button>
</Form>
<div className="text-center text-sm text-bolt-content-tertiary">
<p>
By logging in, you agree to the{' '}
<a href="#" className="text-bolt-content-accent hover:underline">
Terms of Service
</a>{' '}
and{' '}
<a href="#" className="text-bolt-content-accent hover:underline">
Privacy Policy
</a>
</p>
</div>
</div>
</div>
);
}

138
app/routes/auth.logout.tsx Normal file
View File

@@ -0,0 +1,138 @@
import { json, redirect, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from '@remix-run/node';
import { Form, useLoaderData, useNavigation } from '@remix-run/react';
import { useEffect } from 'react';
import { logout, getUserFromSession } from '~/lib/auth/github-oauth.server';
import { updateProfile } from '~/lib/stores/profile';
import BackgroundRays from '~/components/ui/BackgroundRays';
export const meta: MetaFunction = () => {
return [
{ title: 'Logout from Buildify' },
{ name: 'description', content: 'Logout from your Buildify account' },
];
};
export async function loader({ request }: LoaderFunctionArgs) {
// Get URL parameters
const url = new URL(request.url);
const redirectTo = url.searchParams.get('redirectTo') || '/';
// Get user data to display in the confirmation page
const user = await getUserFromSession(request);
return json({
redirectTo,
user,
});
}
export async function action({ request }: ActionFunctionArgs) {
// Get form data
const formData = await request.formData();
const redirectTo = formData.get('redirectTo')?.toString() || '/';
// Perform logout and redirect
return logout(request, redirectTo);
}
export default function Logout() {
const { redirectTo, user } = useLoaderData<typeof loader>();
const navigation = useNavigation();
const isSubmitting = navigation.state === 'submitting';
// Reset profile store on client side when logging out via form submission
useEffect(() => {
if (isSubmitting) {
// Reset profile to empty values
updateProfile({
username: '',
bio: '',
avatar: '',
});
}
}, [isSubmitting]);
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-bolt-elements-background-depth-1">
<BackgroundRays />
<div className="w-full max-w-md p-8 space-y-8 bg-bolt-elements-background-depth-2 rounded-lg shadow-lg">
<div className="text-center">
<h1 className="text-3xl font-bold text-bolt-content-primary">Logout</h1>
{user ? (
<div className="mt-4 flex flex-col items-center">
<img
src={user.avatar_url}
alt={`${user.login}'s avatar`}
className="w-16 h-16 rounded-full border-2 border-bolt-elements-border-primary"
/>
<p className="mt-2 text-bolt-content-primary font-medium">
{user.name || user.login}
</p>
<p className="text-sm text-bolt-content-secondary">
{user.login}
</p>
</div>
) : (
<p className="mt-2 text-bolt-content-secondary">
You are not currently logged in.
</p>
)}
</div>
{user ? (
<Form method="post" className="space-y-6">
<input type="hidden" name="redirectTo" value={redirectTo} />
<p className="text-center text-bolt-content-secondary">
Are you sure you want to log out?
</p>
<div className="flex flex-col space-y-3">
<button
type="submit"
disabled={isSubmitting}
className="w-full py-3 px-4 bg-red-500 hover:bg-red-600 text-white rounded-md transition-colors duration-200 font-medium"
>
{isSubmitting ? (
<span className="flex items-center justify-center">
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Logging out...
</span>
) : (
"Yes, log me out"
)}
</button>
<a
href={redirectTo}
className="w-full py-3 px-4 bg-transparent border border-bolt-elements-border-primary text-bolt-content-primary rounded-md transition-colors duration-200 font-medium text-center"
>
Cancel
</a>
</div>
</Form>
) : (
<div className="space-y-6">
<a
href="/login"
className="block w-full py-3 px-4 bg-[#2da44e] hover:bg-[#2c974b] text-white rounded-md transition-colors duration-200 font-medium text-center"
>
Log in
</a>
<a
href={redirectTo}
className="block w-full py-3 px-4 bg-transparent border border-bolt-elements-border-primary text-bolt-content-primary rounded-md transition-colors duration-200 font-medium text-center"
>
Return to home
</a>
</div>
)}
</div>
</div>
);
}

315
app/routes/profile.tsx Normal file
View File

@@ -0,0 +1,315 @@
import { json, redirect, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from '@remix-run/node';
import { Form, useActionData, useLoaderData, useNavigation } from '@remix-run/react';
import { useEffect, useState } from 'react';
import { getUserFromSession, isAuthenticated, requireAuthentication } from '~/lib/auth/github-oauth.server';
import { useAuth } from '~/lib/hooks/useAuth';
import { updateProfile } from '~/lib/stores/profile';
import BackgroundRays from '~/components/ui/BackgroundRays';
export const meta: MetaFunction = () => {
return [
{ title: 'Your Profile - Buildify' },
{ name: 'description', content: 'Manage your Buildify profile and preferences' },
];
};
export async function loader({ request }: LoaderFunctionArgs) {
// Check if user is authenticated on the server side
const authenticated = await isAuthenticated(request);
if (!authenticated) {
// Redirect to login if not authenticated
return redirect('/auth/login?redirectTo=/profile');
}
// Get user data from session
const user = await getUserFromSession(request);
return json({ user });
}
export async function action({ request }: ActionFunctionArgs) {
// Require authentication for this action
const user = await requireAuthentication(request, '/auth/login?redirectTo=/profile');
// Get form data
const formData = await request.formData();
const bio = formData.get('bio')?.toString() || '';
const displayName = formData.get('displayName')?.toString() || user.login;
const theme = formData.get('theme')?.toString() || 'system';
// Here you would typically update the user data in your database
// Since we're using client-side storage, we'll just return the updated values
// and let the client update the profile store
return json({
success: true,
message: 'Profile updated successfully',
updates: {
bio,
displayName,
theme,
},
});
}
export default function Profile() {
const { user: serverUser } = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
const navigation = useNavigation();
const { isAuthenticated, user, githubUser } = useAuth();
const [bio, setBio] = useState(user?.bio || '');
const [displayName, setDisplayName] = useState(user?.username || serverUser?.login || '');
const isSubmitting = navigation.state === 'submitting';
const lastLogin = user?.lastLogin ? new Date(user.lastLogin).toLocaleString() : 'Unknown';
// Update form values when user data changes
useEffect(() => {
if (user) {
setBio(user.bio || '');
setDisplayName(user.username || '');
}
}, [user]);
// Apply updates from action data
useEffect(() => {
if (actionData?.success && actionData.updates) {
updateProfile({
bio: actionData.updates.bio,
username: actionData.updates.displayName,
});
// Show success notification
// This could be replaced with a toast notification
const timer = setTimeout(() => {
const notification = document.getElementById('notification');
if (notification) {
notification.style.opacity = '0';
}
}, 3000);
return () => clearTimeout(timer);
}
}, [actionData]);
if (!isAuthenticated || !user) {
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-bolt-elements-background-depth-1">
<BackgroundRays />
<div className="w-full max-w-md p-8 bg-bolt-elements-background-depth-2 rounded-lg shadow-lg">
<div className="text-center">
<div className="i-svg-spinners:270-ring-with-bg w-12 h-12 mx-auto text-bolt-content-accent" />
<h1 className="text-2xl font-bold mt-4 text-bolt-content-primary">Loading profile...</h1>
</div>
</div>
</div>
);
}
return (
<div className="flex flex-col min-h-screen bg-bolt-elements-background-depth-1">
<BackgroundRays />
<div className="container mx-auto px-4 py-8 max-w-4xl">
<div className="mb-8">
<h1 className="text-3xl font-bold text-bolt-content-primary">Your Profile</h1>
<p className="text-bolt-content-secondary">
Manage your profile information and preferences
</p>
</div>
{actionData?.success && (
<div
id="notification"
className="mb-6 p-4 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-100 rounded-md transition-opacity duration-500"
>
{actionData.message}
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{/* GitHub Profile Information */}
<div className="col-span-1">
<div className="bg-bolt-elements-background-depth-2 rounded-lg shadow-md p-6 border border-bolt-elements-borderColor">
<div className="flex flex-col items-center">
<img
src={user.avatar || githubUser?.avatar_url}
alt={`${displayName}'s avatar`}
className="w-24 h-24 rounded-full border-2 border-bolt-elements-borderColor"
/>
<h2 className="mt-4 text-xl font-semibold text-bolt-content-primary">
{displayName}
</h2>
<p className="text-sm text-bolt-content-secondary">
@{githubUser?.login || serverUser?.login}
</p>
{githubUser?.html_url && (
<a
href={githubUser.html_url}
target="_blank"
rel="noopener noreferrer"
className="mt-2 text-sm text-bolt-content-accent hover:underline flex items-center"
>
<span className="i-ph:github-logo mr-1" />
GitHub Profile
</a>
)}
<div className="mt-6 w-full">
<div className="py-2 border-t border-bolt-elements-borderColor">
<h3 className="text-sm font-medium text-bolt-content-secondary">Email</h3>
<p className="text-bolt-content-primary">
{githubUser?.email || serverUser?.email || 'Not available'}
</p>
</div>
<div className="py-2 border-t border-bolt-elements-borderColor">
<h3 className="text-sm font-medium text-bolt-content-secondary">Authentication Status</h3>
<div className="flex items-center mt-1">
<span className="inline-block w-2 h-2 rounded-full bg-green-500 mr-2"></span>
<span className="text-bolt-content-primary">Authenticated with GitHub</span>
</div>
</div>
<div className="py-2 border-t border-bolt-elements-borderColor">
<h3 className="text-sm font-medium text-bolt-content-secondary">Last Login</h3>
<p className="text-bolt-content-primary">{lastLogin}</p>
</div>
</div>
</div>
</div>
</div>
{/* Profile Edit Form */}
<div className="col-span-1 md:col-span-2">
<div className="bg-bolt-elements-background-depth-2 rounded-lg shadow-md p-6 border border-bolt-elements-borderColor">
<h2 className="text-xl font-semibold text-bolt-content-primary mb-4">
Edit Profile
</h2>
<Form method="post" className="space-y-6">
<div>
<label htmlFor="displayName" className="block text-sm font-medium text-bolt-content-secondary mb-1">
Display Name
</label>
<input
type="text"
id="displayName"
name="displayName"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
className="w-full px-3 py-2 bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor rounded-md text-bolt-content-primary focus:outline-none focus:ring-2 focus:ring-bolt-content-accent"
/>
</div>
<div>
<label htmlFor="bio" className="block text-sm font-medium text-bolt-content-secondary mb-1">
Bio
</label>
<textarea
id="bio"
name="bio"
rows={4}
value={bio}
onChange={(e) => setBio(e.target.value)}
className="w-full px-3 py-2 bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor rounded-md text-bolt-content-primary focus:outline-none focus:ring-2 focus:ring-bolt-content-accent"
placeholder="Tell us about yourself..."
/>
</div>
<div>
<label htmlFor="theme" className="block text-sm font-medium text-bolt-content-secondary mb-1">
Theme Preference
</label>
<select
id="theme"
name="theme"
className="w-full px-3 py-2 bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor rounded-md text-bolt-content-primary focus:outline-none focus:ring-2 focus:ring-bolt-content-accent"
defaultValue="system"
>
<option value="system">System Default</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<p className="mt-1 text-xs text-bolt-content-tertiary">
Theme preferences will be applied across all your sessions
</p>
</div>
<div className="pt-4">
<button
type="submit"
disabled={isSubmitting}
className={`
w-full sm:w-auto px-4 py-2 rounded-md font-medium
${isSubmitting
? 'bg-bolt-elements-background-depth-3 text-bolt-content-tertiary cursor-not-allowed'
: 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent hover:bg-bolt-elements-item-backgroundAccentHover'}
transition-colors duration-200
`}
>
{isSubmitting ? (
<span className="flex items-center justify-center">
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Saving...
</span>
) : (
'Save Changes'
)}
</button>
</div>
</Form>
</div>
{/* Account Management */}
<div className="mt-6 bg-bolt-elements-background-depth-2 rounded-lg shadow-md p-6 border border-bolt-elements-borderColor">
<h2 className="text-xl font-semibold text-bolt-content-primary mb-4">
Account Management
</h2>
<div className="space-y-4">
<div className="flex justify-between items-center py-2 border-b border-bolt-elements-borderColor">
<div>
<h3 className="font-medium text-bolt-content-primary">Sign Out</h3>
<p className="text-sm text-bolt-content-secondary">
Sign out from your current session
</p>
</div>
<a
href="/auth/logout"
className="px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-md transition-colors duration-200 text-sm font-medium"
>
Sign Out
</a>
</div>
<div className="flex justify-between items-center py-2">
<div>
<h3 className="font-medium text-bolt-content-primary">GitHub Connection</h3>
<p className="text-sm text-bolt-content-secondary">
Manage your GitHub connection settings
</p>
</div>
<a
href="/settings"
className="px-4 py-2 bg-transparent border border-bolt-elements-borderColor hover:bg-bolt-elements-background-depth-3 text-bolt-content-primary rounded-md transition-colors duration-200 text-sm font-medium"
>
Manage
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

592
package-lock.json generated
View File

@@ -37,6 +37,7 @@
"@iconify-json/svg-spinners": "^1.2.1",
"@lezer/highlight": "^1.2.1",
"@nanostores/react": "^0.7.3",
"@octokit/oauth-app": "^8.0.1",
"@octokit/rest": "^21.0.2",
"@octokit/types": "^13.6.2",
"@openrouter/ai-sdk-provider": "^0.0.5",
@@ -60,6 +61,7 @@
"@remix-run/react": "^2.15.2",
"@tanstack/react-virtual": "^3.13.0",
"@types/react-beautiful-dnd": "^13.1.8",
"@types/uuid": "^10.0.0",
"@uiw/codemirror-theme-vscode": "^4.23.6",
"@unocss/reset": "^0.61.9",
"@webcontainer/api": "1.6.1-internal.1",
@@ -115,6 +117,7 @@
"tailwind-merge": "^2.2.1",
"unist-util-visit": "^5.0.0",
"use-debounce": "^10.0.4",
"uuid": "^11.1.0",
"vite-plugin-node-polyfills": "^0.22.0",
"zod": "^3.24.1",
"zustand": "^5.0.3"
@@ -1044,6 +1047,25 @@
"node": ">=18.0.0"
}
},
"node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@types/uuid": {
"version": "9.0.8",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
"license": "MIT"
},
"node_modules/@aws-sdk/client-bedrock-runtime/node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@aws-sdk/client-sso": {
"version": "3.821.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.821.0.tgz",
@@ -4680,6 +4702,269 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/@octokit/auth-oauth-app": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-9.0.1.tgz",
"integrity": "sha512-TthWzYxuHKLAbmxdFZwFlmwVyvynpyPmjwc+2/cI3cvbT7mHtsAW9b1LvQaNnAuWL+pFnqtxdmrU8QpF633i1g==",
"license": "MIT",
"dependencies": {
"@octokit/auth-oauth-device": "^8.0.1",
"@octokit/auth-oauth-user": "^6.0.0",
"@octokit/request": "^10.0.2",
"@octokit/types": "^14.0.0",
"universal-user-agent": "^7.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/auth-oauth-app/node_modules/@octokit/endpoint": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz",
"integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^14.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/auth-oauth-app/node_modules/@octokit/openapi-types": {
"version": "25.1.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
"integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
"license": "MIT"
},
"node_modules/@octokit/auth-oauth-app/node_modules/@octokit/request": {
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.2.tgz",
"integrity": "sha512-iYj4SJG/2bbhh+iIpFmG5u49DtJ4lipQ+aPakjL9OKpsGY93wM8w06gvFbEQxcMsZcCvk5th5KkIm2m8o14aWA==",
"license": "MIT",
"dependencies": {
"@octokit/endpoint": "^11.0.0",
"@octokit/request-error": "^7.0.0",
"@octokit/types": "^14.0.0",
"fast-content-type-parse": "^3.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/auth-oauth-app/node_modules/@octokit/request-error": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz",
"integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^14.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/auth-oauth-app/node_modules/@octokit/types": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
"integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^25.1.0"
}
},
"node_modules/@octokit/auth-oauth-app/node_modules/fast-content-type-parse": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
"integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/@octokit/auth-oauth-device": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-8.0.1.tgz",
"integrity": "sha512-TOqId/+am5yk9zor0RGibmlqn4V0h8vzjxlw/wYr3qzkQxl8aBPur384D1EyHtqvfz0syeXji4OUvKkHvxk/Gw==",
"license": "MIT",
"dependencies": {
"@octokit/oauth-methods": "^6.0.0",
"@octokit/request": "^10.0.2",
"@octokit/types": "^14.0.0",
"universal-user-agent": "^7.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/auth-oauth-device/node_modules/@octokit/endpoint": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz",
"integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^14.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/auth-oauth-device/node_modules/@octokit/openapi-types": {
"version": "25.1.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
"integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
"license": "MIT"
},
"node_modules/@octokit/auth-oauth-device/node_modules/@octokit/request": {
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.2.tgz",
"integrity": "sha512-iYj4SJG/2bbhh+iIpFmG5u49DtJ4lipQ+aPakjL9OKpsGY93wM8w06gvFbEQxcMsZcCvk5th5KkIm2m8o14aWA==",
"license": "MIT",
"dependencies": {
"@octokit/endpoint": "^11.0.0",
"@octokit/request-error": "^7.0.0",
"@octokit/types": "^14.0.0",
"fast-content-type-parse": "^3.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/auth-oauth-device/node_modules/@octokit/request-error": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz",
"integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^14.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/auth-oauth-device/node_modules/@octokit/types": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
"integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^25.1.0"
}
},
"node_modules/@octokit/auth-oauth-device/node_modules/fast-content-type-parse": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
"integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/@octokit/auth-oauth-user": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-6.0.0.tgz",
"integrity": "sha512-GV9IW134PHsLhtUad21WIeP9mlJ+QNpFd6V9vuPWmaiN25HEJeEQUcS4y5oRuqCm9iWDLtfIs+9K8uczBXKr6A==",
"license": "MIT",
"dependencies": {
"@octokit/auth-oauth-device": "^8.0.1",
"@octokit/oauth-methods": "^6.0.0",
"@octokit/request": "^10.0.2",
"@octokit/types": "^14.0.0",
"universal-user-agent": "^7.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/auth-oauth-user/node_modules/@octokit/endpoint": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz",
"integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^14.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/auth-oauth-user/node_modules/@octokit/openapi-types": {
"version": "25.1.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
"integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
"license": "MIT"
},
"node_modules/@octokit/auth-oauth-user/node_modules/@octokit/request": {
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.2.tgz",
"integrity": "sha512-iYj4SJG/2bbhh+iIpFmG5u49DtJ4lipQ+aPakjL9OKpsGY93wM8w06gvFbEQxcMsZcCvk5th5KkIm2m8o14aWA==",
"license": "MIT",
"dependencies": {
"@octokit/endpoint": "^11.0.0",
"@octokit/request-error": "^7.0.0",
"@octokit/types": "^14.0.0",
"fast-content-type-parse": "^3.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/auth-oauth-user/node_modules/@octokit/request-error": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz",
"integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^14.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/auth-oauth-user/node_modules/@octokit/types": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
"integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^25.1.0"
}
},
"node_modules/@octokit/auth-oauth-user/node_modules/fast-content-type-parse": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
"integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/@octokit/auth-token": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz",
@@ -4689,6 +4974,46 @@
"node": ">= 18"
}
},
"node_modules/@octokit/auth-unauthenticated": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@octokit/auth-unauthenticated/-/auth-unauthenticated-7.0.1.tgz",
"integrity": "sha512-qVq1vdjLLZdE8kH2vDycNNjuJRCD1q2oet1nA/GXWaYlpDxlR7rdVhX/K/oszXslXiQIiqrQf+rdhDlA99JdTQ==",
"license": "MIT",
"dependencies": {
"@octokit/request-error": "^7.0.0",
"@octokit/types": "^14.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/auth-unauthenticated/node_modules/@octokit/openapi-types": {
"version": "25.1.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
"integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
"license": "MIT"
},
"node_modules/@octokit/auth-unauthenticated/node_modules/@octokit/request-error": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz",
"integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^14.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/auth-unauthenticated/node_modules/@octokit/types": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
"integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^25.1.0"
}
},
"node_modules/@octokit/core": {
"version": "6.1.5",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.5.tgz",
@@ -4779,6 +5104,240 @@
"@octokit/openapi-types": "^25.1.0"
}
},
"node_modules/@octokit/oauth-app": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/@octokit/oauth-app/-/oauth-app-8.0.1.tgz",
"integrity": "sha512-QnhMYEQpnYbEPn9cae+wXL2LuPMFglmfeuDJXXsyxIXdoORwkLK8y0cHhd/5du9MbO/zdG/BXixzB7EEwU63eQ==",
"license": "MIT",
"dependencies": {
"@octokit/auth-oauth-app": "^9.0.1",
"@octokit/auth-oauth-user": "^6.0.0",
"@octokit/auth-unauthenticated": "^7.0.1",
"@octokit/core": "^7.0.2",
"@octokit/oauth-authorization-url": "^8.0.0",
"@octokit/oauth-methods": "^6.0.0",
"@types/aws-lambda": "^8.10.83",
"universal-user-agent": "^7.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/oauth-app/node_modules/@octokit/auth-token": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz",
"integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==",
"license": "MIT",
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/oauth-app/node_modules/@octokit/core": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.2.tgz",
"integrity": "sha512-ODsoD39Lq6vR6aBgvjTnA3nZGliknKboc9Gtxr7E4WDNqY24MxANKcuDQSF0jzapvGb3KWOEDrKfve4HoWGK+g==",
"license": "MIT",
"dependencies": {
"@octokit/auth-token": "^6.0.0",
"@octokit/graphql": "^9.0.1",
"@octokit/request": "^10.0.2",
"@octokit/request-error": "^7.0.0",
"@octokit/types": "^14.0.0",
"before-after-hook": "^4.0.0",
"universal-user-agent": "^7.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/oauth-app/node_modules/@octokit/endpoint": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz",
"integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^14.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/oauth-app/node_modules/@octokit/graphql": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.1.tgz",
"integrity": "sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg==",
"license": "MIT",
"dependencies": {
"@octokit/request": "^10.0.2",
"@octokit/types": "^14.0.0",
"universal-user-agent": "^7.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/oauth-app/node_modules/@octokit/openapi-types": {
"version": "25.1.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
"integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
"license": "MIT"
},
"node_modules/@octokit/oauth-app/node_modules/@octokit/request": {
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.2.tgz",
"integrity": "sha512-iYj4SJG/2bbhh+iIpFmG5u49DtJ4lipQ+aPakjL9OKpsGY93wM8w06gvFbEQxcMsZcCvk5th5KkIm2m8o14aWA==",
"license": "MIT",
"dependencies": {
"@octokit/endpoint": "^11.0.0",
"@octokit/request-error": "^7.0.0",
"@octokit/types": "^14.0.0",
"fast-content-type-parse": "^3.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/oauth-app/node_modules/@octokit/request-error": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz",
"integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^14.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/oauth-app/node_modules/@octokit/types": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
"integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^25.1.0"
}
},
"node_modules/@octokit/oauth-app/node_modules/before-after-hook": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz",
"integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==",
"license": "Apache-2.0"
},
"node_modules/@octokit/oauth-app/node_modules/fast-content-type-parse": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
"integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/@octokit/oauth-authorization-url": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-8.0.0.tgz",
"integrity": "sha512-7QoLPRh/ssEA/HuHBHdVdSgF8xNLz/Bc5m9fZkArJE5bb6NmVkDm3anKxXPmN1zh6b5WKZPRr3697xKT/yM3qQ==",
"license": "MIT",
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/oauth-methods": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@octokit/oauth-methods/-/oauth-methods-6.0.0.tgz",
"integrity": "sha512-Q8nFIagNLIZgM2odAraelMcDssapc+lF+y3OlcIPxyAU+knefO8KmozGqfnma1xegRDP4z5M73ABsamn72bOcA==",
"license": "MIT",
"dependencies": {
"@octokit/oauth-authorization-url": "^8.0.0",
"@octokit/request": "^10.0.2",
"@octokit/request-error": "^7.0.0",
"@octokit/types": "^14.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/oauth-methods/node_modules/@octokit/endpoint": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.0.tgz",
"integrity": "sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^14.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/oauth-methods/node_modules/@octokit/openapi-types": {
"version": "25.1.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
"integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
"license": "MIT"
},
"node_modules/@octokit/oauth-methods/node_modules/@octokit/request": {
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.2.tgz",
"integrity": "sha512-iYj4SJG/2bbhh+iIpFmG5u49DtJ4lipQ+aPakjL9OKpsGY93wM8w06gvFbEQxcMsZcCvk5th5KkIm2m8o14aWA==",
"license": "MIT",
"dependencies": {
"@octokit/endpoint": "^11.0.0",
"@octokit/request-error": "^7.0.0",
"@octokit/types": "^14.0.0",
"fast-content-type-parse": "^3.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/oauth-methods/node_modules/@octokit/request-error": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.0.tgz",
"integrity": "sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^14.0.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/@octokit/oauth-methods/node_modules/@octokit/types": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
"integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^25.1.0"
}
},
"node_modules/@octokit/oauth-methods/node_modules/fast-content-type-parse": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
"integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/@octokit/openapi-types": {
"version": "24.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
@@ -7179,6 +7738,19 @@
"node": ">=18.0.0"
}
},
"node_modules/@smithy/middleware-retry/node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@smithy/middleware-serde": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.8.tgz",
@@ -7735,6 +8307,12 @@
"@types/estree": "*"
}
},
"node_modules/@types/aws-lambda": {
"version": "8.10.149",
"resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.149.tgz",
"integrity": "sha512-NXSZIhfJjnXqJgtS7IwutqIF/SOy1Wz5Px4gUY1RWITp3AYTyuJS4xaXr/bIJY1v15XMzrJ5soGnPM+7uigZjA==",
"license": "MIT"
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -8055,9 +8633,9 @@
"license": "MIT"
},
"node_modules/@types/uuid": {
"version": "9.0.8",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
"integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==",
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==",
"license": "MIT"
},
"node_modules/@types/verror": {
@@ -31350,16 +31928,16 @@
}
},
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/uvu": {

View File

@@ -72,6 +72,7 @@
"@iconify-json/svg-spinners": "^1.2.1",
"@lezer/highlight": "^1.2.1",
"@nanostores/react": "^0.7.3",
"@octokit/oauth-app": "^8.0.1",
"@octokit/rest": "^21.0.2",
"@octokit/types": "^13.6.2",
"@openrouter/ai-sdk-provider": "^0.0.5",
@@ -95,6 +96,7 @@
"@remix-run/react": "^2.15.2",
"@tanstack/react-virtual": "^3.13.0",
"@types/react-beautiful-dnd": "^13.1.8",
"@types/uuid": "^10.0.0",
"@uiw/codemirror-theme-vscode": "^4.23.6",
"@unocss/reset": "^0.61.9",
"@webcontainer/api": "1.6.1-internal.1",
@@ -150,6 +152,7 @@
"tailwind-merge": "^2.2.1",
"unist-util-visit": "^5.0.0",
"use-debounce": "^10.0.4",
"uuid": "^11.1.0",
"vite-plugin-node-polyfills": "^0.22.0",
"zod": "^3.24.1",
"zustand": "^5.0.3"