From b80ffee3f75924c94b6cdfd0e4dbb521c9f074c7 Mon Sep 17 00:00:00 2001 From: Jake Shore Date: Wed, 18 Feb 2026 21:52:50 -0500 Subject: [PATCH] Compass mock mode - runs locally without WorkOS auth - Middleware bypasses WorkOS when API keys not configured - AuthWrapper conditionally loads AuthKitProvider - getCurrentUser() returns mock dev user when no auth - Demo mode (/demo) sets cookie for dashboard access - Memory DB provider fallback when D1 unavailable - CF dev proxy gated behind ENABLE_CF_DEV=1 - All changes conditional - real auth works if keys provided --- next.config.ts | 13 +++++++++- src/app/actions/dashboards.ts | 1 + src/app/layout.tsx | 6 ++--- src/components/auth-wrapper.tsx | 26 +++++++++++++++++++ src/db/index.ts | 27 ++++++++++++++++++++ src/lib/auth.ts | 4 ++- src/middleware.ts | 44 +++++++++++++++++++++++++++++++-- 7 files changed, 114 insertions(+), 7 deletions(-) create mode 100644 src/components/auth-wrapper.tsx diff --git a/next.config.ts b/next.config.ts index 8f3e6a3..a01766e 100755 --- a/next.config.ts +++ b/next.config.ts @@ -27,8 +27,19 @@ export default nextConfig; // Enable calling `getCloudflareContext()` in `next dev`. // See https://opennext.js.org/cloudflare/bindings#local-access-to-bindings. // Only init in dev -- build and lint don't need the wrangler proxy. -if (process.env.NODE_ENV === "development") { +// Disabled for local dev without Cloudflare account access: +if (process.env.NODE_ENV === "development" && process.env.ENABLE_CF_DEV === "1") { import("@opennextjs/cloudflare").then((mod) => mod.initOpenNextCloudflareForDev() ); +} else if (process.env.NODE_ENV === "development") { + // When Cloudflare dev proxy is not enabled, set a mock context so + // getCloudflareContext() doesn't throw. Actions check `env?.DB` and + // gracefully return empty data when it's missing. + const sym = Symbol.for("__cloudflare-context__"); + (globalThis as Record)[sym] = { + env: {}, // no DB binding — actions will short-circuit + cf: {}, + ctx: { waitUntil: () => {}, passThroughOnException: () => {} }, + }; } diff --git a/src/app/actions/dashboards.ts b/src/app/actions/dashboards.ts index 4e50154..01140bf 100755 --- a/src/app/actions/dashboards.ts +++ b/src/app/actions/dashboards.ts @@ -37,6 +37,7 @@ export async function getCustomDashboards(): Promise< if (!user) return { success: false, error: "not authenticated" } const { env } = await getCloudflareContext() + if (!env?.DB) return { success: true, data: [] } const db = getDb(env.DB) const dashboards = await db.query.customDashboards.findMany({ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7bae8b4..5784abd 100755 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,7 +1,7 @@ import type { Metadata } from "next"; import { Sora, IBM_Plex_Mono, Playfair_Display } from "next/font/google"; -import { AuthKitProvider } from "@workos-inc/authkit-nextjs/components"; import { ThemeProvider } from "@/components/theme-provider"; +import { AuthWrapper } from "@/components/auth-wrapper"; import "./globals.css"; const sora = Sora({ @@ -44,11 +44,11 @@ export default function RootLayout({ return ( - + {children} - + ); diff --git a/src/components/auth-wrapper.tsx b/src/components/auth-wrapper.tsx new file mode 100644 index 0000000..8f7596a --- /dev/null +++ b/src/components/auth-wrapper.tsx @@ -0,0 +1,26 @@ +import type { ReactNode } from "react" + +/** + * Server component that conditionally renders the real AuthKitProvider + * (when WorkOS is configured) or a simple passthrough (demo/local mode). + * + * This avoids importing @workos-inc/authkit-nextjs/components when + * WORKOS_API_KEY is empty, which would throw NoApiKeyProvidedException. + */ + +const isWorkOSConfigured = + !!process.env.WORKOS_API_KEY && + !!process.env.WORKOS_CLIENT_ID && + !process.env.WORKOS_API_KEY.includes("placeholder") + +export async function AuthWrapper({ children }: { children: ReactNode }) { + if (isWorkOSConfigured) { + // Dynamic import so the module is never loaded when WorkOS is absent + const { AuthKitProvider } = await import( + "@workos-inc/authkit-nextjs/components" + ) + return {children} + } + + return <>{children} +} diff --git a/src/db/index.ts b/src/db/index.ts index 52beb92..27edb70 100755 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -23,9 +23,36 @@ const allSchemas = { ...conversationsSchema, } +/** + * Null-safe stub returned when no D1 binding is available (local dev without CF). + * Every property access returns a chainable proxy that resolves to empty results, + * so server actions that forget to check `env?.DB` won't crash. + */ +function createNullDb(): ReturnType { + const handler: ProxyHandler = { + get(_target, prop) { + // .then — make the proxy non-thenable so `await proxy` returns the proxy itself + if (prop === "then") return undefined + // Common drizzle terminal methods — resolve to empty/noop + if (prop === "all" || prop === "values") return async () => [] + if (prop === "get") return async () => undefined + if (prop === "run") return async () => ({ changes: 0 }) + if (prop === "execute") return async () => [] + // findMany / findFirst on the relational query builder + if (prop === "findMany") return async () => [] + if (prop === "findFirst") return async () => undefined + // Everything else returns another proxy so chaining works: + // db.select().from(t).where(...) etc. + return new Proxy((..._args: unknown[]) => new Proxy({}, handler), handler) + }, + } + return new Proxy({}, handler) as ReturnType +} + // Legacy function - kept for backwards compatibility // Prefer using the provider interface from ./provider for new code export function getDb(d1: D1Database) { + if (!d1) return createNullDb() return drizzle(d1, { schema: allSchemas }) } diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 2c6d597..eeacd63 100755 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,4 +1,3 @@ -import { withAuth, signOut } from "@workos-inc/authkit-nextjs" import { getCloudflareContext } from "@opennextjs/cloudflare" import { getDb } from "@/db" import { users, organizations, organizationMembers } from "@/db/schema" @@ -91,6 +90,7 @@ export async function getCurrentUser(): Promise { } // WorkOS is configured -- try real auth first + const { withAuth } = await import("@workos-inc/authkit-nextjs") const session = await withAuth() if (!session || !session.user) { @@ -281,6 +281,7 @@ export async function ensureUserExists(workosUser: { } export async function handleSignOut() { + const { signOut } = await import("@workos-inc/authkit-nextjs") await signOut() } @@ -302,6 +303,7 @@ export async function requireEmailVerified(): Promise { !process.env.WORKOS_API_KEY.includes("placeholder") if (isWorkOSConfigured) { + const { withAuth } = await import("@workos-inc/authkit-nextjs") const session = await withAuth() if (session?.user && !session.user.emailVerified) { throw new Error("Email not verified") diff --git a/src/middleware.ts b/src/middleware.ts index f958938..07879ef 100755 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,5 +1,4 @@ -import { NextRequest } from "next/server" -import { authkit, handleAuthkitHeaders } from "@workos-inc/authkit-nextjs" +import { NextRequest, NextResponse } from "next/server" // public routes that don't require authentication const publicPaths = [ @@ -31,7 +30,48 @@ function isPublicPath(pathname: string): boolean { ) } +/** + * When WorkOS is not configured, skip authkit entirely and passthrough. + * Protected routes require the demo cookie; otherwise redirect to login. + */ +function handleNoWorkOS(request: NextRequest): NextResponse { + const { pathname } = request.nextUrl + + if (isPublicPath(pathname)) { + return NextResponse.next() + } + + const hasDemoCookie = + request.cookies.get("compass-demo")?.value === "true" + + if (hasDemoCookie) { + return NextResponse.next() + } + + // no session & not demo — redirect to login + const loginUrl = new URL("/login", request.url) + loginUrl.searchParams.set("from", pathname) + return NextResponse.redirect(loginUrl) +} + export default async function middleware(request: NextRequest) { + // When WorkOS is not configured, skip authkit entirely to avoid + // NoApiKeyProvidedException. The app falls back to demo/dev user + // via src/lib/auth.ts. + const isWorkOSConfigured = + process.env.WORKOS_API_KEY && + process.env.WORKOS_CLIENT_ID && + !process.env.WORKOS_API_KEY.includes("placeholder") + + if (!isWorkOSConfigured) { + return handleNoWorkOS(request) + } + + // --- WorkOS IS configured: use authkit as normal --- + const { authkit, handleAuthkitHeaders } = await import( + "@workos-inc/authkit-nextjs" + ) + const { pathname } = request.nextUrl // get session and headers from authkit (handles token refresh automatically)