diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..002382503 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "submodules/chat"] + path = submodules/chat + url = https://github.com/lightfastai/chat.git diff --git a/apps/www/next.config.ts b/apps/www/next.config.ts index 81801dad1..e6e43b6c4 100644 --- a/apps/www/next.config.ts +++ b/apps/www/next.config.ts @@ -2,58 +2,58 @@ import { NextConfig } from "next"; import "~/env"; +import { mergeNextConfig } from "@vendor/next/merge-config"; import { - config as vendorConfig, - withBetterStack, - withSentry, + config as vendorConfig, + withBetterStack, + withSentry, } from "@vendor/next/next-config-builder"; -import { mergeNextConfig } from "@vendor/next/merge-config"; import { env } from "~/env"; let config: NextConfig = withBetterStack( - mergeNextConfig(vendorConfig, { - reactStrictMode: true, - - /** Enables hot reloading for local packages without a build step */ - transpilePackages: [ - "@repo/ui", - "@vendor/security", - "@vendor/analytics", - "@vendor/email", - "@vendor/clerk", - "@vendor/inngest", - "@vendor/observability", - "@vendor/next", - "@vendor/upstash", - "@repo/lightfast-config", - "@repo/lightfast-email", - "@repo/lightfast-react", - "@repo/lib", - ], - - /** We already do linting and typechecking as separate tasks in CI */ - // eslint: { ignoreDuringBuilds: true }, - // typescript: { ignoreBuildErrors: true }, - - // Add automatic static optimization where possible - experimental: { - // For Next.js 15.3+ - optimizeCss: true, - optimizePackageImports: [ - "@repo/ui", - "jotai", - "lucide-react", - "react-confetti", - ], - // Faster navigation for production - // ppr: true, - }, - }) + mergeNextConfig(vendorConfig, { + reactStrictMode: true, + + /** Enables hot reloading for local packages without a build step */ + transpilePackages: [ + "@repo/ui", + "@vendor/security", + "@vendor/analytics", + "@vendor/email", + "@vendor/clerk", + "@vendor/inngest", + "@vendor/observability", + "@vendor/next", + "@vendor/upstash", + "@repo/lightfast-config", + "@repo/lightfast-email", + "@repo/lightfast-react", + "@repo/lib", + ], + + /** We already do linting and typechecking as separate tasks in CI */ + // eslint: { ignoreDuringBuilds: true }, + // typescript: { ignoreBuildErrors: true }, + + // Add automatic static optimization where possible + experimental: { + // For Next.js 15.3+ + optimizeCss: true, + optimizePackageImports: [ + "@repo/ui", + "jotai", + "lucide-react", + "react-confetti", + ], + // Faster navigation for production + // ppr: true, + }, + }), ); if (env.VERCEL) { - config = withSentry(config); + config = withSentry(config); } export default config; diff --git a/apps/www/src/app/(app)/(landing)/page.tsx b/apps/www/src/app/(app)/(landing)/page.tsx index 94de46005..ea9932ef3 100644 --- a/apps/www/src/app/(app)/(landing)/page.tsx +++ b/apps/www/src/app/(app)/(landing)/page.tsx @@ -4,42 +4,42 @@ import { ZapIcon } from "lucide-react"; import { siteConfig } from "@repo/lightfast-config"; import { Icons } from "@repo/ui/components/icons"; -import { Button } from "@repo/ui/components/ui/button"; import { LightfastCustomGridBackground } from "@repo/ui/components/lightfast-custom-grid-background"; +import { Button } from "@repo/ui/components/ui/button"; export default function HomePage() { - return ( - - -
-
-

- Crafting tomorrow's AI backbone with open-source infrastructure. -

-
+ return ( + + +
+
+

+ Crafting tomorrow's AI backbone with open-source infrastructure. +

+
-
- -
+
+ +
-
- -
-
-
-
- ); +
+ +
+
+
+
+ ); } diff --git a/apps/www/src/app/(health)/api/health/route.ts b/apps/www/src/app/(health)/api/health/route.ts index 24ac9675e..b0cabb9f6 100644 --- a/apps/www/src/app/(health)/api/health/route.ts +++ b/apps/www/src/app/(health)/api/health/route.ts @@ -1,16 +1,17 @@ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; + import { env } from "~/env"; export const runtime = "edge"; /** * Health check endpoint for monitoring services like BetterStack. - * + * * Authentication: * - If HEALTH_CHECK_AUTH_TOKEN is set, requires Bearer token authentication * - If not set, endpoint is public (for development/testing) - * + * * BetterStack Configuration: * 1. Set HTTP method: GET * 2. Add header: Authorization: Bearer @@ -19,63 +20,58 @@ export const runtime = "edge"; export function GET(request: NextRequest) { // Check if authentication is configured const authToken = env.HEALTH_CHECK_AUTH_TOKEN; - + if (authToken) { // Extract bearer token from Authorization header const authHeader = request.headers.get("authorization"); - + if (!authHeader) { return NextResponse.json( { error: "Authorization required" }, - { status: 401 } + { status: 401 }, ); } - + // Check for Bearer token format const bearerRegex = /^Bearer\s+(.+)$/i; const bearerMatch = bearerRegex.exec(authHeader); if (!bearerMatch) { return NextResponse.json( { error: "Invalid authorization format" }, - { status: 401 } + { status: 401 }, ); } - + const providedToken = bearerMatch[1]; - + // Constant-time comparison to prevent timing attacks const encoder = new TextEncoder(); const expectedBytes = encoder.encode(authToken); const providedBytes = encoder.encode(providedToken); - + if (expectedBytes.length !== providedBytes.length) { - return NextResponse.json( - { error: "Unauthorized" }, - { status: 401 } - ); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } - + let valid = true; for (let i = 0; i < expectedBytes.length; i++) { if (expectedBytes[i] !== providedBytes[i]) { valid = false; } } - + if (!valid) { - return NextResponse.json( - { error: "Unauthorized" }, - { status: 401 } - ); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } } - + // Log monitoring access for observability (optional) const userAgent = request.headers.get("user-agent") ?? "unknown"; - const isLikelyBetterStack = userAgent.toLowerCase().includes("betterstack") || - userAgent.toLowerCase().includes("uptime") || - userAgent.toLowerCase().includes("monitor"); - + const isLikelyBetterStack = + userAgent.toLowerCase().includes("betterstack") || + userAgent.toLowerCase().includes("uptime") || + userAgent.toLowerCase().includes("monitor"); + // Return health status const response = NextResponse.json( { @@ -100,12 +96,12 @@ export function GET(request: NextRequest) { status: 200, }, ); - + // Prevent caching of health check responses response.headers.set("Cache-Control", "no-store, no-cache, must-revalidate"); response.headers.set("Pragma", "no-cache"); response.headers.set("Expires", "0"); - + return response; } @@ -116,7 +112,7 @@ export function OPTIONS() { return new Response(null, { status: 204, headers: { - "Allow": "GET, OPTIONS", + Allow: "GET, OPTIONS", "Cache-Control": "no-store", }, }); diff --git a/apps/www/src/app/global-error.tsx b/apps/www/src/app/global-error.tsx index 2f8435f34..09918f126 100644 --- a/apps/www/src/app/global-error.tsx +++ b/apps/www/src/app/global-error.tsx @@ -7,10 +7,13 @@ import { captureException } from "@sentry/nextjs"; import { GeistMono } from "geist/font/mono"; import { GeistSans } from "geist/font/sans"; +import { LightfastCustomGridBackground } from "@repo/ui/components/lightfast-custom-grid-background"; +import { + ErrorCode, + LightfastErrorPage, +} from "@repo/ui/components/lightfast-error-page"; import { Button } from "@repo/ui/components/ui/button"; import { cn } from "@repo/ui/lib/utils"; -import { LightfastCustomGridBackground } from "@repo/ui/components/lightfast-custom-grid-background"; -import { LightfastErrorPage, ErrorCode } from "@repo/ui/components/lightfast-error-page"; interface GlobalErrorProperties { readonly error: NextError & { digest?: string }; diff --git a/apps/www/src/app/layout.tsx b/apps/www/src/app/layout.tsx index 5e875869d..42d606545 100644 --- a/apps/www/src/app/layout.tsx +++ b/apps/www/src/app/layout.tsx @@ -12,108 +12,108 @@ import { SpeedInsights, VercelAnalytics } from "@vendor/analytics/vercel"; import { createBaseUrl } from "~/lib/base-url"; export const metadata: Metadata = { - title: { - default: siteConfig.name, - template: `%s - ${siteConfig.name}`, - }, - metadataBase: new URL(siteConfig.url), - description: siteConfig.description, - keywords: [ - "Lightfast", - "AI", - "Design", - "Blender", - "Unreal Engine", - "Ableton", - "MCP", - "Model Context Protocol", - "AI Copilot", - "AI Copilot for Creatives", - "AI Copilot for Blender", - "AI Copilot for TouchDesigner", - "AI Copilot for Houdini", - "AI Copilot for Unreal Engine", - ], - authors: [ - { - name: siteConfig.name, - url: siteConfig.url, - }, - ], - creator: siteConfig.name, - openGraph: { - type: "website", - locale: "en_US", - url: siteConfig.url, - title: siteConfig.name, - description: siteConfig.description, - siteName: siteConfig.name, - images: [ - { - url: siteConfig.ogImage, - width: 1200, - height: 630, - alt: siteConfig.name, - }, - ], - }, - twitter: { - card: "summary_large_image", - title: siteConfig.name, - description: siteConfig.description, - images: [siteConfig.ogImage], - creator: siteConfig.links.twitter.href, - }, - icons: { - icon: "/favicon.ico", - shortcut: "/favicon-16x16.png", - apple: "/apple-touch-icon.png", - other: [ - { - rel: "icon", - url: "/favicon-32x32.png", - }, - { - rel: "icon", - url: "/android-chrome-192x192.png", - }, - { - rel: "icon", - url: "/android-chrome-512x512.png", - }, - ], - }, - applicationName: siteConfig.name, - appleWebApp: { - capable: true, - statusBarStyle: "default", - title: siteConfig.name, - }, - formatDetection: { - telephone: false, - }, + title: { + default: siteConfig.name, + template: `%s - ${siteConfig.name}`, + }, + metadataBase: new URL(siteConfig.url), + description: siteConfig.description, + keywords: [ + "Lightfast", + "AI", + "Design", + "Blender", + "Unreal Engine", + "Ableton", + "MCP", + "Model Context Protocol", + "AI Copilot", + "AI Copilot for Creatives", + "AI Copilot for Blender", + "AI Copilot for TouchDesigner", + "AI Copilot for Houdini", + "AI Copilot for Unreal Engine", + ], + authors: [ + { + name: siteConfig.name, + url: siteConfig.url, + }, + ], + creator: siteConfig.name, + openGraph: { + type: "website", + locale: "en_US", + url: siteConfig.url, + title: siteConfig.name, + description: siteConfig.description, + siteName: siteConfig.name, + images: [ + { + url: siteConfig.ogImage, + width: 1200, + height: 630, + alt: siteConfig.name, + }, + ], + }, + twitter: { + card: "summary_large_image", + title: siteConfig.name, + description: siteConfig.description, + images: [siteConfig.ogImage], + creator: siteConfig.links.twitter.href, + }, + icons: { + icon: "/favicon.ico", + shortcut: "/favicon-16x16.png", + apple: "/apple-touch-icon.png", + other: [ + { + rel: "icon", + url: "/favicon-32x32.png", + }, + { + rel: "icon", + url: "/android-chrome-192x192.png", + }, + { + rel: "icon", + url: "/android-chrome-512x512.png", + }, + ], + }, + applicationName: siteConfig.name, + appleWebApp: { + capable: true, + statusBarStyle: "default", + title: siteConfig.name, + }, + formatDetection: { + telephone: false, + }, }; export const viewport: Viewport = { - themeColor: "#09090b", + themeColor: "#09090b", }; export default function RootLayout({ - children, + children, }: Readonly<{ - children: React.ReactNode; + children: React.ReactNode; }>) { - return ( - - - - - {children} - - - - - - - ); + return ( + + + + + {children} + + + + + + + ); } diff --git a/apps/www/src/app/not-found.tsx b/apps/www/src/app/not-found.tsx index 623b38de0..4e2e1dcb5 100644 --- a/apps/www/src/app/not-found.tsx +++ b/apps/www/src/app/not-found.tsx @@ -1,26 +1,30 @@ import Link from "next/link"; -import { Button } from "@repo/ui/components/ui/button"; + import { LightfastCustomGridBackground } from "@repo/ui/components/lightfast-custom-grid-background"; -import { LightfastErrorPage, ErrorCode } from "@repo/ui/components/lightfast-error-page"; +import { + ErrorCode, + LightfastErrorPage, +} from "@repo/ui/components/lightfast-error-page"; +import { Button } from "@repo/ui/components/ui/button"; export default function NotFound() { - return ( - - - - - - - - ); + return ( + + + + + + + + ); } diff --git a/apps/www/src/components/early-access/early-access-form.tsx b/apps/www/src/components/early-access/early-access-form.tsx index 07ee4e050..944fb7fd2 100644 --- a/apps/www/src/components/early-access/early-access-form.tsx +++ b/apps/www/src/components/early-access/early-access-form.tsx @@ -2,13 +2,12 @@ import type * as z from "zod"; import { useState } from "react"; +import { zodResolver } from "@hookform/resolvers/zod"; import { useAtom } from "jotai"; import { Send } from "lucide-react"; import Confetti from "react-confetti"; import { createPortal } from "react-dom"; - import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; import { Button } from "@repo/ui/components/ui/button"; import { diff --git a/apps/www/src/components/site-footer.tsx b/apps/www/src/components/site-footer.tsx index 968eff695..eca96d7ee 100644 --- a/apps/www/src/components/site-footer.tsx +++ b/apps/www/src/components/site-footer.tsx @@ -1,9 +1,10 @@ -import { cn } from "@repo/ui/lib/utils" -import { Dot } from "lucide-react" -import Link from "next/link" +import Link from "next/link"; +import { Dot } from "lucide-react"; + +import { cn } from "@repo/ui/lib/utils"; interface FooterProps { - className?: string + className?: string; } export function SiteFooter({ className }: FooterProps) { @@ -112,5 +113,5 @@ export function SiteFooter({ className }: FooterProps) { - ) + ); } diff --git a/apps/www/src/middleware.ts b/apps/www/src/middleware.ts index d9508027b..80f87adce 100644 --- a/apps/www/src/middleware.ts +++ b/apps/www/src/middleware.ts @@ -2,8 +2,8 @@ import type { NextRequest } from "next/server"; import { NextResponse } from "next/server"; import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server"; -import { log } from "@vendor/observability/log"; import { getAllAppUrls, getClerkMiddlewareConfig } from "@repo/url-utils"; +import { log } from "@vendor/observability/log"; import { generateSignedRequestId, @@ -15,48 +15,51 @@ const clerkConfig = getClerkMiddlewareConfig("www"); // Define public routes that don't require authentication const isPublicRoute = createRouteMatcher([ "/", - "/api/health", + "/api/health", "/api/early-access/(.*)", - "/legal/(.*)" + "/legal/(.*)", ]); /** * Middleware to handle request ID generation, authentication, and protected routes */ -export const middleware = clerkMiddleware(async (auth, request: NextRequest) => { - // Handle authentication protection first - if (!isPublicRoute(request)) { - await auth.protect(); - } - - const { userId } = await auth(); - - // If user is authenticated and on landing page, redirect to app - if (userId && request.nextUrl.pathname === "/") { - const urls = getAllAppUrls(); - return NextResponse.redirect(new URL(urls.app)); - } +export const middleware = clerkMiddleware( + async (auth, request: NextRequest) => { + // Handle authentication protection first + if (!isPublicRoute(request)) { + await auth.protect(); + } + + const { userId } = await auth(); + + // If user is authenticated and on landing page, redirect to app + if (userId && request.nextUrl.pathname === "/") { + const urls = getAllAppUrls(); + return NextResponse.redirect(new URL(urls.app)); + } - // Generate a new request ID for all requests if one doesn't exist - const existingRequestId = request.headers.get(REQUEST_ID_HEADER); - const requestId = existingRequestId ?? (await generateSignedRequestId(log)); + // Generate a new request ID for all requests if one doesn't exist + const existingRequestId = request.headers.get(REQUEST_ID_HEADER); + const requestId = existingRequestId ?? (await generateSignedRequestId(log)); - // Clone the request headers and set the request ID - const requestHeaders = new Headers(request.headers); - requestHeaders.set(REQUEST_ID_HEADER, requestId); + // Clone the request headers and set the request ID + const requestHeaders = new Headers(request.headers); + requestHeaders.set(REQUEST_ID_HEADER, requestId); - // Return response with both request and response headers set - return NextResponse.next({ - request: { - // New request headers - headers: requestHeaders, - }, - headers: { - // Also set response header for consistency - [REQUEST_ID_HEADER]: requestId, - }, - }); -}, clerkConfig); + // Return response with both request and response headers set + return NextResponse.next({ + request: { + // New request headers + headers: requestHeaders, + }, + headers: { + // Also set response header for consistency + [REQUEST_ID_HEADER]: requestId, + }, + }); + }, + clerkConfig, +); export const config = { matcher: [ @@ -65,4 +68,4 @@ export const config = { // Always run for API routes except Inngest "/(api(?!/inngest)|trpc)(.*)", ], -} +}; diff --git a/submodules/chat b/submodules/chat new file mode 160000 index 000000000..6ebdd4b58 --- /dev/null +++ b/submodules/chat @@ -0,0 +1 @@ +Subproject commit 6ebdd4b58a303183f757b8874b5bf56afb893e90