Dominic Elm
		
	commited on
		
		
					Commit 
							
							·
						
						d2b36e8
	
1
								Parent(s):
							
							6927c07
								
feat: add login
Browse files- .github/workflows/ci.yaml +1 -1
 - .tool-versions +1 -1
 - README.md +1 -1
 - package.json +1 -1
 - packages/bolt/README.md +1 -1
 - packages/bolt/app/lib/.server/login.ts +7 -0
 - packages/bolt/app/lib/.server/sessions.ts +55 -0
 - packages/bolt/app/routes/_index.tsx +12 -1
 - packages/bolt/app/routes/login.tsx +90 -0
 - packages/bolt/worker-configuration.d.ts +2 -0
 - pnpm-lock.yaml +25 -3
 
    	
        .github/workflows/ci.yaml
    CHANGED
    
    | 
         @@ -12,7 +12,7 @@ jobs: 
     | 
|
| 12 | 
         
             
                runs-on: ubuntu-latest
         
     | 
| 13 | 
         
             
                strategy:
         
     | 
| 14 | 
         
             
                  matrix:
         
     | 
| 15 | 
         
            -
                    node-version: [ 
     | 
| 16 | 
         
             
                steps:
         
     | 
| 17 | 
         
             
                  - name: Setup
         
     | 
| 18 | 
         
             
                    uses: pnpm/action-setup@v4
         
     | 
| 
         | 
|
| 12 | 
         
             
                runs-on: ubuntu-latest
         
     | 
| 13 | 
         
             
                strategy:
         
     | 
| 14 | 
         
             
                  matrix:
         
     | 
| 15 | 
         
            +
                    node-version: [20.15.1]
         
     | 
| 16 | 
         
             
                steps:
         
     | 
| 17 | 
         
             
                  - name: Setup
         
     | 
| 18 | 
         
             
                    uses: pnpm/action-setup@v4
         
     | 
    	
        .tool-versions
    CHANGED
    
    | 
         @@ -1,2 +1,2 @@ 
     | 
|
| 1 | 
         
            -
            nodejs  
     | 
| 2 | 
         
             
            pnpm 9.4.0
         
     | 
| 
         | 
|
| 1 | 
         
            +
            nodejs 20.15.1
         
     | 
| 2 | 
         
             
            pnpm 9.4.0
         
     | 
    	
        README.md
    CHANGED
    
    | 
         @@ -14,7 +14,7 @@ As the project grows, additional packages may be added to this workspace. 
     | 
|
| 14 | 
         | 
| 15 | 
         
             
            ### Prerequisites
         
     | 
| 16 | 
         | 
| 17 | 
         
            -
            - Node.js ( 
     | 
| 18 | 
         
             
            - pnpm (v9.4.0)
         
     | 
| 19 | 
         | 
| 20 | 
         
             
            ### Installation
         
     | 
| 
         | 
|
| 14 | 
         | 
| 15 | 
         
             
            ### Prerequisites
         
     | 
| 16 | 
         | 
| 17 | 
         
            +
            - Node.js (v20.15.1)
         
     | 
| 18 | 
         
             
            - pnpm (v9.4.0)
         
     | 
| 19 | 
         | 
| 20 | 
         
             
            ### Installation
         
     | 
    	
        package.json
    CHANGED
    
    | 
         @@ -22,7 +22,7 @@ 
     | 
|
| 22 | 
         
             
                }
         
     | 
| 23 | 
         
             
              },
         
     | 
| 24 | 
         
             
              "engines": {
         
     | 
| 25 | 
         
            -
                "node": " 
     | 
| 26 | 
         
             
                "pnpm": "9.4.0"
         
     | 
| 27 | 
         
             
              },
         
     | 
| 28 | 
         
             
              "devDependencies": {
         
     | 
| 
         | 
|
| 22 | 
         
             
                }
         
     | 
| 23 | 
         
             
              },
         
     | 
| 24 | 
         
             
              "engines": {
         
     | 
| 25 | 
         
            +
                "node": "20.15.1",
         
     | 
| 26 | 
         
             
                "pnpm": "9.4.0"
         
     | 
| 27 | 
         
             
              },
         
     | 
| 28 | 
         
             
              "devDependencies": {
         
     | 
    	
        packages/bolt/README.md
    CHANGED
    
    | 
         @@ -6,7 +6,7 @@ Bolt is an AI assistant developed by StackBlitz. This package contains the UI in 
     | 
|
| 6 | 
         | 
| 7 | 
         
             
            Before you begin, ensure you have the following installed:
         
     | 
| 8 | 
         | 
| 9 | 
         
            -
            - Node.js ( 
     | 
| 10 | 
         
             
            - pnpm (v9.4.0)
         
     | 
| 11 | 
         | 
| 12 | 
         
             
            ## Setup
         
     | 
| 
         | 
|
| 6 | 
         | 
| 7 | 
         
             
            Before you begin, ensure you have the following installed:
         
     | 
| 8 | 
         | 
| 9 | 
         
            +
            - Node.js (v20.15.1)
         
     | 
| 10 | 
         
             
            - pnpm (v9.4.0)
         
     | 
| 11 | 
         | 
| 12 | 
         
             
            ## Setup
         
     | 
    	
        packages/bolt/app/lib/.server/login.ts
    ADDED
    
    | 
         @@ -0,0 +1,7 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            import { env } from 'node:process';
         
     | 
| 2 | 
         
            +
             
     | 
| 3 | 
         
            +
            export function verifyPassword(password: string, cloudflareEnv: Env) {
         
     | 
| 4 | 
         
            +
              const loginPassword = env.LOGIN_PASSWORD || cloudflareEnv.LOGIN_PASSWORD;
         
     | 
| 5 | 
         
            +
             
     | 
| 6 | 
         
            +
              return password === loginPassword;
         
     | 
| 7 | 
         
            +
            }
         
     | 
    	
        packages/bolt/app/lib/.server/sessions.ts
    ADDED
    
    | 
         @@ -0,0 +1,55 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            import { createCookieSessionStorage, redirect } from '@remix-run/cloudflare';
         
     | 
| 2 | 
         
            +
            import { env } from 'node:process';
         
     | 
| 3 | 
         
            +
             
     | 
| 4 | 
         
            +
            const USER_SESSION_KEY = 'userId';
         
     | 
| 5 | 
         
            +
             
     | 
| 6 | 
         
            +
            function createSessionStorage(cloudflareEnv: Env) {
         
     | 
| 7 | 
         
            +
              return createCookieSessionStorage({
         
     | 
| 8 | 
         
            +
                cookie: {
         
     | 
| 9 | 
         
            +
                  name: '__session',
         
     | 
| 10 | 
         
            +
                  httpOnly: true,
         
     | 
| 11 | 
         
            +
                  path: '/',
         
     | 
| 12 | 
         
            +
                  sameSite: 'lax',
         
     | 
| 13 | 
         
            +
                  secrets: [env.SESSION_SECRET || cloudflareEnv.SESSION_SECRET],
         
     | 
| 14 | 
         
            +
                  secure: false,
         
     | 
| 15 | 
         
            +
                },
         
     | 
| 16 | 
         
            +
              });
         
     | 
| 17 | 
         
            +
            }
         
     | 
| 18 | 
         
            +
             
     | 
| 19 | 
         
            +
            export async function getSession(request: Request, env: Env) {
         
     | 
| 20 | 
         
            +
              const sessionStorage = createSessionStorage(env);
         
     | 
| 21 | 
         
            +
              const cookie = request.headers.get('Cookie');
         
     | 
| 22 | 
         
            +
             
     | 
| 23 | 
         
            +
              return { session: await sessionStorage.getSession(cookie), sessionStorage };
         
     | 
| 24 | 
         
            +
            }
         
     | 
| 25 | 
         
            +
             
     | 
| 26 | 
         
            +
            export async function logout(request: Request, env: Env) {
         
     | 
| 27 | 
         
            +
              const { session, sessionStorage } = await getSession(request, env);
         
     | 
| 28 | 
         
            +
             
     | 
| 29 | 
         
            +
              return redirect('/login', {
         
     | 
| 30 | 
         
            +
                headers: {
         
     | 
| 31 | 
         
            +
                  'Set-Cookie': await sessionStorage.destroySession(session),
         
     | 
| 32 | 
         
            +
                },
         
     | 
| 33 | 
         
            +
              });
         
     | 
| 34 | 
         
            +
            }
         
     | 
| 35 | 
         
            +
             
     | 
| 36 | 
         
            +
            export async function isAuthenticated(request: Request, env: Env) {
         
     | 
| 37 | 
         
            +
              const { session } = await getSession(request, env);
         
     | 
| 38 | 
         
            +
              const userId = session.get(USER_SESSION_KEY);
         
     | 
| 39 | 
         
            +
             
     | 
| 40 | 
         
            +
              return !!userId;
         
     | 
| 41 | 
         
            +
            }
         
     | 
| 42 | 
         
            +
             
     | 
| 43 | 
         
            +
            export async function createUserSession(request: Request, env: Env): Promise<ResponseInit> {
         
     | 
| 44 | 
         
            +
              const { session, sessionStorage } = await getSession(request, env);
         
     | 
| 45 | 
         
            +
             
     | 
| 46 | 
         
            +
              session.set(USER_SESSION_KEY, 'anonymous_user');
         
     | 
| 47 | 
         
            +
             
     | 
| 48 | 
         
            +
              return {
         
     | 
| 49 | 
         
            +
                headers: {
         
     | 
| 50 | 
         
            +
                  'Set-Cookie': await sessionStorage.commitSession(session, {
         
     | 
| 51 | 
         
            +
                    maxAge: 60 * 60 * 24 * 7, // 7 days,
         
     | 
| 52 | 
         
            +
                  }),
         
     | 
| 53 | 
         
            +
                },
         
     | 
| 54 | 
         
            +
              };
         
     | 
| 55 | 
         
            +
            }
         
     | 
    	
        packages/bolt/app/routes/_index.tsx
    CHANGED
    
    | 
         @@ -1,13 +1,24 @@ 
     | 
|
| 1 | 
         
            -
            import type  
     | 
| 2 | 
         
             
            import { ClientOnly } from 'remix-utils/client-only';
         
     | 
| 3 | 
         
             
            import { BaseChat } from '~/components/chat/BaseChat';
         
     | 
| 4 | 
         
             
            import { Chat } from '~/components/chat/Chat.client';
         
     | 
| 5 | 
         
             
            import { Header } from '~/components/Header';
         
     | 
| 
         | 
|
| 6 | 
         | 
| 7 | 
         
             
            export const meta: MetaFunction = () => {
         
     | 
| 8 | 
         
             
              return [{ title: 'Bolt' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }];
         
     | 
| 9 | 
         
             
            };
         
     | 
| 10 | 
         | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 11 | 
         
             
            export default function Index() {
         
     | 
| 12 | 
         
             
              return (
         
     | 
| 13 | 
         
             
                <div className="flex flex-col h-full w-full">
         
     | 
| 
         | 
|
| 1 | 
         
            +
            import { json, redirect, type LoaderFunctionArgs, type MetaFunction } from '@remix-run/cloudflare';
         
     | 
| 2 | 
         
             
            import { ClientOnly } from 'remix-utils/client-only';
         
     | 
| 3 | 
         
             
            import { BaseChat } from '~/components/chat/BaseChat';
         
     | 
| 4 | 
         
             
            import { Chat } from '~/components/chat/Chat.client';
         
     | 
| 5 | 
         
             
            import { Header } from '~/components/Header';
         
     | 
| 6 | 
         
            +
            import { isAuthenticated } from '~/lib/.server/sessions';
         
     | 
| 7 | 
         | 
| 8 | 
         
             
            export const meta: MetaFunction = () => {
         
     | 
| 9 | 
         
             
              return [{ title: 'Bolt' }, { name: 'description', content: 'Talk with Bolt, an AI assistant from StackBlitz' }];
         
     | 
| 10 | 
         
             
            };
         
     | 
| 11 | 
         | 
| 12 | 
         
            +
            export async function loader({ request, context }: LoaderFunctionArgs) {
         
     | 
| 13 | 
         
            +
              const authenticated = await isAuthenticated(request, context.cloudflare.env);
         
     | 
| 14 | 
         
            +
             
     | 
| 15 | 
         
            +
              if (import.meta.env.DEV || authenticated) {
         
     | 
| 16 | 
         
            +
                return json({});
         
     | 
| 17 | 
         
            +
              }
         
     | 
| 18 | 
         
            +
             
     | 
| 19 | 
         
            +
              return redirect('/login');
         
     | 
| 20 | 
         
            +
            }
         
     | 
| 21 | 
         
            +
             
     | 
| 22 | 
         
             
            export default function Index() {
         
     | 
| 23 | 
         
             
              return (
         
     | 
| 24 | 
         
             
                <div className="flex flex-col h-full w-full">
         
     | 
    	
        packages/bolt/app/routes/login.tsx
    ADDED
    
    | 
         @@ -0,0 +1,90 @@ 
     | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
| 
         | 
|
| 1 | 
         
            +
            import {
         
     | 
| 2 | 
         
            +
              json,
         
     | 
| 3 | 
         
            +
              redirect,
         
     | 
| 4 | 
         
            +
              type ActionFunctionArgs,
         
     | 
| 5 | 
         
            +
              type LoaderFunctionArgs,
         
     | 
| 6 | 
         
            +
              type TypedResponse,
         
     | 
| 7 | 
         
            +
            } from '@remix-run/cloudflare';
         
     | 
| 8 | 
         
            +
            import { Form, useActionData } from '@remix-run/react';
         
     | 
| 9 | 
         
            +
            import { verifyPassword } from '~/lib/.server/login';
         
     | 
| 10 | 
         
            +
            import { createUserSession, isAuthenticated } from '~/lib/.server/sessions';
         
     | 
| 11 | 
         
            +
             
     | 
| 12 | 
         
            +
            interface Errors {
         
     | 
| 13 | 
         
            +
              password?: string;
         
     | 
| 14 | 
         
            +
            }
         
     | 
| 15 | 
         
            +
             
     | 
| 16 | 
         
            +
            export async function loader({ request, context }: LoaderFunctionArgs) {
         
     | 
| 17 | 
         
            +
              const authenticated = await isAuthenticated(request, context.cloudflare.env);
         
     | 
| 18 | 
         
            +
             
     | 
| 19 | 
         
            +
              if (authenticated) {
         
     | 
| 20 | 
         
            +
                return redirect('/');
         
     | 
| 21 | 
         
            +
              }
         
     | 
| 22 | 
         
            +
             
     | 
| 23 | 
         
            +
              return json({});
         
     | 
| 24 | 
         
            +
            }
         
     | 
| 25 | 
         
            +
             
     | 
| 26 | 
         
            +
            export async function action({ request, context }: ActionFunctionArgs): Promise<TypedResponse<{ errors?: Errors }>> {
         
     | 
| 27 | 
         
            +
              const formData = await request.formData();
         
     | 
| 28 | 
         
            +
              const password = String(formData.get('password'));
         
     | 
| 29 | 
         
            +
             
     | 
| 30 | 
         
            +
              const errors: Errors = {};
         
     | 
| 31 | 
         
            +
             
     | 
| 32 | 
         
            +
              if (!password) {
         
     | 
| 33 | 
         
            +
                errors.password = 'Please provide a password';
         
     | 
| 34 | 
         
            +
              }
         
     | 
| 35 | 
         
            +
             
     | 
| 36 | 
         
            +
              if (!verifyPassword(password, context.cloudflare.env)) {
         
     | 
| 37 | 
         
            +
                errors.password = 'Invalid password';
         
     | 
| 38 | 
         
            +
              }
         
     | 
| 39 | 
         
            +
             
     | 
| 40 | 
         
            +
              if (Object.keys(errors).length > 0) {
         
     | 
| 41 | 
         
            +
                return json({ errors });
         
     | 
| 42 | 
         
            +
              }
         
     | 
| 43 | 
         
            +
             
     | 
| 44 | 
         
            +
              return redirect('/', await createUserSession(request, context.cloudflare.env));
         
     | 
| 45 | 
         
            +
            }
         
     | 
| 46 | 
         
            +
             
     | 
| 47 | 
         
            +
            export default function Login() {
         
     | 
| 48 | 
         
            +
              const actionData = useActionData<typeof action>();
         
     | 
| 49 | 
         
            +
             
     | 
| 50 | 
         
            +
              return (
         
     | 
| 51 | 
         
            +
                <div className="min-h-screen flex items-center justify-center">
         
     | 
| 52 | 
         
            +
                  <div className="max-w-md w-full space-y-8 p-10 bg-white rounded-lg shadow">
         
     | 
| 53 | 
         
            +
                    <div>
         
     | 
| 54 | 
         
            +
                      <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">Login</h2>
         
     | 
| 55 | 
         
            +
                    </div>
         
     | 
| 56 | 
         
            +
                    <Form className="mt-8 space-y-6" method="post" noValidate>
         
     | 
| 57 | 
         
            +
                      <div>
         
     | 
| 58 | 
         
            +
                        <label htmlFor="password" className="sr-only">
         
     | 
| 59 | 
         
            +
                          Password
         
     | 
| 60 | 
         
            +
                        </label>
         
     | 
| 61 | 
         
            +
                        <input
         
     | 
| 62 | 
         
            +
                          id="password"
         
     | 
| 63 | 
         
            +
                          name="password"
         
     | 
| 64 | 
         
            +
                          type="password"
         
     | 
| 65 | 
         
            +
                          autoComplete="off"
         
     | 
| 66 | 
         
            +
                          data-1p-ignore
         
     | 
| 67 | 
         
            +
                          required
         
     | 
| 68 | 
         
            +
                          className="appearance-none rounded-md relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 focus:outline-none"
         
     | 
| 69 | 
         
            +
                          placeholder="Password"
         
     | 
| 70 | 
         
            +
                        />
         
     | 
| 71 | 
         
            +
                        {actionData?.errors?.password ? (
         
     | 
| 72 | 
         
            +
                          <em className="flex items-center space-x-1.5 p-2 mt-2 bg-negative-200 text-negative-600 rounded-lg">
         
     | 
| 73 | 
         
            +
                            <div className="i-ph:x-circle text-xl"></div>
         
     | 
| 74 | 
         
            +
                            <span>{actionData?.errors.password}</span>
         
     | 
| 75 | 
         
            +
                          </em>
         
     | 
| 76 | 
         
            +
                        ) : null}
         
     | 
| 77 | 
         
            +
                      </div>
         
     | 
| 78 | 
         
            +
                      <div>
         
     | 
| 79 | 
         
            +
                        <button
         
     | 
| 80 | 
         
            +
                          type="submit"
         
     | 
| 81 | 
         
            +
                          className="w-full text-white bg-accent-600 hover:bg-accent-700 focus:ring-4 focus:outline-none font-medium rounded-lg text-sm px-5 py-2.5 text-center"
         
     | 
| 82 | 
         
            +
                        >
         
     | 
| 83 | 
         
            +
                          Login
         
     | 
| 84 | 
         
            +
                        </button>
         
     | 
| 85 | 
         
            +
                      </div>
         
     | 
| 86 | 
         
            +
                    </Form>
         
     | 
| 87 | 
         
            +
                  </div>
         
     | 
| 88 | 
         
            +
                </div>
         
     | 
| 89 | 
         
            +
              );
         
     | 
| 90 | 
         
            +
            }
         
     | 
    	
        packages/bolt/worker-configuration.d.ts
    CHANGED
    
    | 
         @@ -1,3 +1,5 @@ 
     | 
|
| 1 | 
         
             
            interface Env {
         
     | 
| 2 | 
         
             
              ANTHROPIC_API_KEY: string;
         
     | 
| 
         | 
|
| 
         | 
|
| 3 | 
         
             
            }
         
     | 
| 
         | 
|
| 1 | 
         
             
            interface Env {
         
     | 
| 2 | 
         
             
              ANTHROPIC_API_KEY: string;
         
     | 
| 3 | 
         
            +
              SESSION_SECRET: string;
         
     | 
| 4 | 
         
            +
              LOGIN_PASSWORD: string;
         
     | 
| 5 | 
         
             
            }
         
     | 
    	
        pnpm-lock.yaml
    CHANGED
    
    | 
         @@ -100,7 +100,7 @@ importers: 
     | 
|
| 100 | 
         
             
                    version: 4.0.0
         
     | 
| 101 | 
         
             
                  remix-utils:
         
     | 
| 102 | 
         
             
                    specifier: ^7.6.0
         
     | 
| 103 | 
         
            -
                    version: 7.6.0(@remix-run/cloudflare@2.10.2(@cloudflare/workers-types@4.20240620.0)(typescript@5.5.2))(@remix-run/node@2.10. 
     | 
| 104 | 
         
             
                  shiki:
         
     | 
| 105 | 
         
             
                    specifier: ^1.9.1
         
     | 
| 106 | 
         
             
                    version: 1.9.1
         
     | 
| 
         @@ -1102,6 +1102,15 @@ packages: 
     | 
|
| 1102 | 
         
             
                  typescript:
         
     | 
| 1103 | 
         
             
                    optional: true
         
     | 
| 1104 | 
         | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 1105 | 
         
             
              '@remix-run/react@2.10.2':
         
     | 
| 1106 | 
         
             
                resolution: {integrity: sha512-0Fx3AYNjfn6Z/0xmIlVC7exmof20M429PwuApWF1H8YXwdkI+cxLfivRzTa1z7vS55tshurqQum98jQQaUDjoA==}
         
     | 
| 1107 | 
         
             
                engines: {node: '>=18.0.0'}
         
     | 
| 
         @@ -5558,6 +5567,19 @@ snapshots: 
     | 
|
| 5558 | 
         
             
                optionalDependencies:
         
     | 
| 5559 | 
         
             
                  typescript: 5.5.2
         
     | 
| 5560 | 
         | 
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 
         | 
|
| 5561 | 
         
             
              '@remix-run/react@2.10.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.2)':
         
     | 
| 5562 | 
         
             
                dependencies:
         
     | 
| 5563 | 
         
             
                  '@remix-run/router': 1.17.1
         
     | 
| 
         @@ -8911,12 +8933,12 @@ snapshots: 
     | 
|
| 8911 | 
         
             
                  mdast-util-to-markdown: 2.1.0
         
     | 
| 8912 | 
         
             
                  unified: 11.0.5
         
     | 
| 8913 | 
         | 
| 8914 | 
         
            -
              remix-utils@7.6.0(@remix-run/cloudflare@2.10.2(@cloudflare/workers-types@4.20240620.0)(typescript@5.5.2))(@remix-run/node@2.10. 
     | 
| 8915 | 
         
             
                dependencies:
         
     | 
| 8916 | 
         
             
                  type-fest: 4.21.0
         
     | 
| 8917 | 
         
             
                optionalDependencies:
         
     | 
| 8918 | 
         
             
                  '@remix-run/cloudflare': 2.10.2(@cloudflare/workers-types@4.20240620.0)(typescript@5.5.2)
         
     | 
| 8919 | 
         
            -
                  '@remix-run/node': 2.10. 
     | 
| 8920 | 
         
             
                  '@remix-run/react': 2.10.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.2)
         
     | 
| 8921 | 
         
             
                  '@remix-run/router': 1.17.1
         
     | 
| 8922 | 
         
             
                  react: 18.3.1
         
     | 
| 
         | 
|
| 100 | 
         
             
                    version: 4.0.0
         
     | 
| 101 | 
         
             
                  remix-utils:
         
     | 
| 102 | 
         
             
                    specifier: ^7.6.0
         
     | 
| 103 | 
         
            +
                    version: 7.6.0(@remix-run/cloudflare@2.10.2(@cloudflare/workers-types@4.20240620.0)(typescript@5.5.2))(@remix-run/node@2.10.2(typescript@5.5.2))(@remix-run/react@2.10.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.2))(@remix-run/router@1.17.1)(react@18.3.1)(zod@3.23.8)
         
     | 
| 104 | 
         
             
                  shiki:
         
     | 
| 105 | 
         
             
                    specifier: ^1.9.1
         
     | 
| 106 | 
         
             
                    version: 1.9.1
         
     | 
| 
         | 
|
| 1102 | 
         
             
                  typescript:
         
     | 
| 1103 | 
         
             
                    optional: true
         
     | 
| 1104 | 
         | 
| 1105 | 
         
            +
              '@remix-run/node@2.10.2':
         
     | 
| 1106 | 
         
            +
                resolution: {integrity: sha512-Ni4yMQCf6avK2fz91/luuS3wnHzqtbxsdc19es1gAWEnUKfeCwqq5v1R0kzNwrXyh5NYCRhxaegzVH3tGsdYFg==}
         
     | 
| 1107 | 
         
            +
                engines: {node: '>=18.0.0'}
         
     | 
| 1108 | 
         
            +
                peerDependencies:
         
     | 
| 1109 | 
         
            +
                  typescript: ^5.1.0
         
     | 
| 1110 | 
         
            +
                peerDependenciesMeta:
         
     | 
| 1111 | 
         
            +
                  typescript:
         
     | 
| 1112 | 
         
            +
                    optional: true
         
     | 
| 1113 | 
         
            +
             
     | 
| 1114 | 
         
             
              '@remix-run/react@2.10.2':
         
     | 
| 1115 | 
         
             
                resolution: {integrity: sha512-0Fx3AYNjfn6Z/0xmIlVC7exmof20M429PwuApWF1H8YXwdkI+cxLfivRzTa1z7vS55tshurqQum98jQQaUDjoA==}
         
     | 
| 1116 | 
         
             
                engines: {node: '>=18.0.0'}
         
     | 
| 
         | 
|
| 5567 | 
         
             
                optionalDependencies:
         
     | 
| 5568 | 
         
             
                  typescript: 5.5.2
         
     | 
| 5569 | 
         | 
| 5570 | 
         
            +
              '@remix-run/node@2.10.2(typescript@5.5.2)':
         
     | 
| 5571 | 
         
            +
                dependencies:
         
     | 
| 5572 | 
         
            +
                  '@remix-run/server-runtime': 2.10.2(typescript@5.5.2)
         
     | 
| 5573 | 
         
            +
                  '@remix-run/web-fetch': 4.4.2
         
     | 
| 5574 | 
         
            +
                  '@web3-storage/multipart-parser': 1.0.0
         
     | 
| 5575 | 
         
            +
                  cookie-signature: 1.2.1
         
     | 
| 5576 | 
         
            +
                  source-map-support: 0.5.21
         
     | 
| 5577 | 
         
            +
                  stream-slice: 0.1.2
         
     | 
| 5578 | 
         
            +
                  undici: 6.19.2
         
     | 
| 5579 | 
         
            +
                optionalDependencies:
         
     | 
| 5580 | 
         
            +
                  typescript: 5.5.2
         
     | 
| 5581 | 
         
            +
                optional: true
         
     | 
| 5582 | 
         
            +
             
     | 
| 5583 | 
         
             
              '@remix-run/react@2.10.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.2)':
         
     | 
| 5584 | 
         
             
                dependencies:
         
     | 
| 5585 | 
         
             
                  '@remix-run/router': 1.17.1
         
     | 
| 
         | 
|
| 8933 | 
         
             
                  mdast-util-to-markdown: 2.1.0
         
     | 
| 8934 | 
         
             
                  unified: 11.0.5
         
     | 
| 8935 | 
         | 
| 8936 | 
         
            +
              remix-utils@7.6.0(@remix-run/cloudflare@2.10.2(@cloudflare/workers-types@4.20240620.0)(typescript@5.5.2))(@remix-run/node@2.10.2(typescript@5.5.2))(@remix-run/react@2.10.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.2))(@remix-run/router@1.17.1)(react@18.3.1)(zod@3.23.8):
         
     | 
| 8937 | 
         
             
                dependencies:
         
     | 
| 8938 | 
         
             
                  type-fest: 4.21.0
         
     | 
| 8939 | 
         
             
                optionalDependencies:
         
     | 
| 8940 | 
         
             
                  '@remix-run/cloudflare': 2.10.2(@cloudflare/workers-types@4.20240620.0)(typescript@5.5.2)
         
     | 
| 8941 | 
         
            +
                  '@remix-run/node': 2.10.2(typescript@5.5.2)
         
     | 
| 8942 | 
         
             
                  '@remix-run/react': 2.10.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.5.2)
         
     | 
| 8943 | 
         
             
                  '@remix-run/router': 1.17.1
         
     | 
| 8944 | 
         
             
                  react: 18.3.1
         
     |