From 5218fc19c547bed6d6886bd7ea5c34065f35da00 Mon Sep 17 00:00:00 2001 From: luci-efe Date: Thu, 9 Apr 2026 06:38:19 -0600 Subject: [PATCH 01/11] =?UTF-8?q?feat(auth):=20auth=20client=20SDK,=20useA?= =?UTF-8?q?uth=20hook,=20vite=20proxy=20=E2=80=94=20SPEC-20260409-002=20(A?= =?UTF-8?q?gent=20A:=2019/19=20tests)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PROJECT_STATE.md | 1 + frontend/package-lock.json | 342 +++++++++++ frontend/package.json | 3 +- frontend/src/hooks/use-auth.ts | 28 +- frontend/src/lib/auth-client.ts | 15 + frontend/vite.config.ts | 12 + package-lock.json | 779 ++++++++++++++++++++++++ specs/active/SPEC-20260409-002.md | 314 ++++++++++ tests/fe-auth-pages/auth-client.test.ts | 147 +++++ tests/fe-auth-pages/vite-proxy.test.ts | 77 +++ 10 files changed, 1707 insertions(+), 11 deletions(-) create mode 100644 frontend/src/lib/auth-client.ts create mode 100644 specs/active/SPEC-20260409-002.md create mode 100644 tests/fe-auth-pages/auth-client.test.ts create mode 100644 tests/fe-auth-pages/vite-proxy.test.ts diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index f79cadd..b6a1890 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -12,6 +12,7 @@ | SPEC-20260409-001 | better-auth-drizzle-libsql-auth | QA | 2026-04-09 | Better Auth + Drizzle/LibSQL: schema, Better Auth instance, drizzle-kit push, Zod schemas, route mounting in Mastra | | SPEC-20260408-002 | linear-resend-integration-tools | CODE | 2026-04-08 | Linear ticketing tools, Resend email tools, shared schemas, and runtime integration tests | | SPEC-20260408-003 | langfuse-observability-integration | CODE | 2026-04-08 | OpenRouter Broadcast + Langfuse SDK fallback, Cloudflare tunnel, trace verification | +| SPEC-20260409-002 | fe-auth-pages-login-register | CODE | 2026-04-09 | Auth pages (login/register), Better Auth client SDK, auth guard, neumorphic UX | ## Integration Test Status diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ec76383..97f82b0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -18,6 +18,7 @@ "@tanstack/react-query": "^5.96.2", "@tanstack/react-router": "^1.168.10", "ai": "^6.0.153", + "better-auth": "^1.6.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.7.0", @@ -491,6 +492,147 @@ "node": ">=6.9.0" } }, + "node_modules/@better-auth/core": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@better-auth/core/-/core-1.6.1.tgz", + "integrity": "sha512-HnRQEuuCT7tIc2GMuRGqudt0DNO8+LhoR3O1heY/nYMxbxPVOBm/f4UQFXeb5HYcHpDrwTO+woWpp318OQ++Uw==", + "license": "MIT", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.39.0", + "@standard-schema/spec": "^1.1.0", + "zod": "^4.3.6" + }, + "peerDependencies": { + "@better-auth/utils": "0.4.0", + "@better-fetch/fetch": "1.1.21", + "@cloudflare/workers-types": ">=4", + "@opentelemetry/api": "^1.9.0", + "better-call": "1.3.5", + "jose": "^6.1.0", + "kysely": "^0.28.5", + "nanostores": "^1.0.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/@better-auth/drizzle-adapter": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@better-auth/drizzle-adapter/-/drizzle-adapter-1.6.1.tgz", + "integrity": "sha512-DgFDG8emuBxHYHA1xfUSCtMExsxOw6W3A1AVLA8TzYIg4qjkfDXcnI40ZPkDfsdnYm2WwQgJYRWP5KXlESakGA==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.1", + "@better-auth/utils": "0.4.0", + "drizzle-orm": ">=0.41.0" + }, + "peerDependenciesMeta": { + "drizzle-orm": { + "optional": true + } + } + }, + "node_modules/@better-auth/kysely-adapter": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@better-auth/kysely-adapter/-/kysely-adapter-1.6.1.tgz", + "integrity": "sha512-P3X/nTpY5EoBD9HEF5I4gTCGL3lf7wIlzdQweL1+WIlJK0yw6jaSqyne82PvgbLs63BLKODrQ8KNy9yrQKwPBw==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.1", + "@better-auth/utils": "0.4.0", + "kysely": "^0.27.0 || ^0.28.0" + }, + "peerDependenciesMeta": { + "kysely": { + "optional": true + } + } + }, + "node_modules/@better-auth/memory-adapter": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@better-auth/memory-adapter/-/memory-adapter-1.6.1.tgz", + "integrity": "sha512-avyECOAv5LsSlIPoQpM++iMlc34YRde/B8klfbJpnqFE25NXtDTyxlv0xkp+RWETWqrnHs4vC+rLePylK7LwGg==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.1", + "@better-auth/utils": "0.4.0" + } + }, + "node_modules/@better-auth/mongo-adapter": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@better-auth/mongo-adapter/-/mongo-adapter-1.6.1.tgz", + "integrity": "sha512-ijK+5eePMV5Vgy88lvUgn0eP5jeWvV5wYPtpFFTu+n+jCpe5VEcqlpTdYiSP3s3MhW9QucoNg08ov0hhaiM83Q==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.1", + "@better-auth/utils": "0.4.0", + "mongodb": "^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "mongodb": { + "optional": true + } + } + }, + "node_modules/@better-auth/prisma-adapter": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@better-auth/prisma-adapter/-/prisma-adapter-1.6.1.tgz", + "integrity": "sha512-KqNHKHFQM+Pc2SF/k5DbVAUzCXZqMIDx4QpFAmDt3Wo0tzvGl12g6/ph8bOM8LKScZAVPpkm3z+NRDbh5sKuuw==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.1", + "@better-auth/utils": "0.4.0", + "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", + "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" + }, + "peerDependenciesMeta": { + "@prisma/client": { + "optional": true + }, + "prisma": { + "optional": true + } + } + }, + "node_modules/@better-auth/telemetry": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@better-auth/telemetry/-/telemetry-1.6.1.tgz", + "integrity": "sha512-UrY49vZ7RHvMlsVlivgvIsCECdl2DAQfmqI8aymY0GMWE6MjukrZ3iS+L+txKWrV7I8vt5itc468ntR1PuvbKw==", + "license": "MIT", + "peerDependencies": { + "@better-auth/core": "^1.6.1", + "@better-auth/utils": "0.4.0", + "@better-fetch/fetch": "1.1.21" + } + }, + "node_modules/@better-auth/utils": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.4.0.tgz", + "integrity": "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^2.0.1" + } + }, + "node_modules/@better-auth/utils/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@better-fetch/fetch": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.21.tgz", + "integrity": "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==" + }, "node_modules/@dotenvx/dotenvx": { "version": "1.60.2", "license": "BSD-3-Clause", @@ -1554,6 +1696,15 @@ "node": ">=8.0.0" } }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz", + "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "license": "MIT" @@ -4589,6 +4740,155 @@ "node": ">=6.0.0" } }, + "node_modules/better-auth": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.6.1.tgz", + "integrity": "sha512-rXxR5G0xNezCiwlxdvU5L0BsYvlddvGfL61azo1w8IFrqck0jPOuE9zU28ZFJpQjfTNwXhWp0ddEqxY7zVZxmQ==", + "license": "MIT", + "dependencies": { + "@better-auth/core": "1.6.1", + "@better-auth/drizzle-adapter": "1.6.1", + "@better-auth/kysely-adapter": "1.6.1", + "@better-auth/memory-adapter": "1.6.1", + "@better-auth/mongo-adapter": "1.6.1", + "@better-auth/prisma-adapter": "1.6.1", + "@better-auth/telemetry": "1.6.1", + "@better-auth/utils": "0.4.0", + "@better-fetch/fetch": "1.1.21", + "@noble/ciphers": "^2.1.1", + "@noble/hashes": "^2.0.1", + "better-call": "1.3.5", + "defu": "^6.1.4", + "jose": "^6.1.3", + "kysely": "^0.28.14", + "nanostores": "^1.1.1", + "zod": "^4.3.6" + }, + "peerDependencies": { + "@lynx-js/react": "*", + "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", + "@sveltejs/kit": "^2.0.0", + "@tanstack/react-start": "^1.0.0", + "@tanstack/solid-start": "^1.0.0", + "better-sqlite3": "^12.0.0", + "drizzle-kit": ">=0.31.4", + "drizzle-orm": ">=0.41.0", + "mongodb": "^6.0.0 || ^7.0.0", + "mysql2": "^3.0.0", + "next": "^14.0.0 || ^15.0.0 || ^16.0.0", + "pg": "^8.0.0", + "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0", + "solid-js": "^1.0.0", + "svelte": "^4.0.0 || ^5.0.0", + "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", + "vue": "^3.0.0" + }, + "peerDependenciesMeta": { + "@lynx-js/react": { + "optional": true + }, + "@prisma/client": { + "optional": true + }, + "@sveltejs/kit": { + "optional": true + }, + "@tanstack/react-start": { + "optional": true + }, + "@tanstack/solid-start": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "drizzle-kit": { + "optional": true + }, + "drizzle-orm": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "next": { + "optional": true + }, + "pg": { + "optional": true + }, + "prisma": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "solid-js": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vitest": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/better-auth/node_modules/@noble/ciphers": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz", + "integrity": "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/better-auth/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/better-call": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.3.5.tgz", + "integrity": "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA==", + "license": "MIT", + "dependencies": { + "@better-auth/utils": "^0.4.0", + "@better-fetch/fetch": "^1.1.21", + "rou3": "^0.7.12", + "set-cookie-parser": "^3.0.1" + }, + "peerDependencies": { + "zod": "^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -5198,6 +5498,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, "node_modules/depd": { "version": "2.0.0", "license": "MIT", @@ -6638,6 +6944,15 @@ "node": ">=6" } }, + "node_modules/kysely": { + "version": "0.28.15", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.15.tgz", + "integrity": "sha512-r2clcf7HLWvDXaVUEvQymXJY4i3bSOIV3xsL/Upy3ZfSv5HeKsk9tsqbBptLvth5qHEIhxeHTA2jNLyQABkLBA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/levn": { "version": "0.4.1", "dev": true, @@ -8019,6 +8334,21 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/nanostores": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.2.0.tgz", + "integrity": "sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": "^20.0.0 || >=22.0.0" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "dev": true, @@ -9037,6 +9367,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/rou3": { + "version": "0.7.12", + "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.7.12.tgz", + "integrity": "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==", + "license": "MIT" + }, "node_modules/router": { "version": "2.2.0", "license": "MIT", @@ -9167,6 +9503,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/set-cookie-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", + "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "license": "ISC" diff --git a/frontend/package.json b/frontend/package.json index b4f1509..b4fe730 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,17 +17,18 @@ "@fontsource-variable/inter": "^5.2.8", "@fontsource-variable/jetbrains-mono": "^5.2.8", "@fontsource-variable/space-grotesk": "^5.2.10", + "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.2.1", "@tanstack/react-query": "^5.96.2", "@tanstack/react-router": "^1.168.10", "ai": "^6.0.153", + "better-auth": "^1.6.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.7.0", "radix-ui": "^1.4.3", "react": "^19.2.4", "react-dom": "^19.2.4", - "@tailwindcss/typography": "^0.5.16", "react-markdown": "^10.1.0", "remark-gfm": "^4.0.1", "shadcn": "^4.2.0", diff --git a/frontend/src/hooks/use-auth.ts b/frontend/src/hooks/use-auth.ts index 2b27f12..6df1e04 100644 --- a/frontend/src/hooks/use-auth.ts +++ b/frontend/src/hooks/use-auth.ts @@ -1,10 +1,8 @@ /** - * Better Auth client hook. - * Chenko will implement this as part of TRI-4 (Better Auth Setup) - * and TRI-21 (Auth Pages). - * - * This placeholder exports the hook shape so routes can reference it. + * Better Auth session hook. + * Wraps authClient.useSession() from Better Auth React SDK. */ +import { authClient, signIn, signUp, signOut } from "@/lib/auth-client" export interface User { id: string @@ -19,11 +17,21 @@ export interface AuthState { } export function useAuth(): AuthState { - // TODO: Replace with Better Auth client SDK - // import { createAuthClient } from "better-auth/react" + const session = authClient.useSession() + + const user: User | null = session.data?.user + ? { + id: session.data.user.id, + email: session.data.user.email, + name: session.data.user.name, + } + : null + return { - user: null, - isLoading: false, - isAuthenticated: false, + user, + isLoading: session.isPending, + isAuthenticated: !!session.data?.user, } } + +export { signIn, signUp, signOut } diff --git a/frontend/src/lib/auth-client.ts b/frontend/src/lib/auth-client.ts new file mode 100644 index 0000000..6da0e34 --- /dev/null +++ b/frontend/src/lib/auth-client.ts @@ -0,0 +1,15 @@ +/** + * Better Auth client SDK — configured to match backend auth at /auth. + * In dev mode, Vite proxy forwards /auth/* to localhost:4111. + */ +import { createAuthClient } from "better-auth/react" + +export const authClient = createAuthClient({ + baseURL: + typeof window !== "undefined" + ? window.location.origin + : "http://localhost:3001", + basePath: "/auth", +}) + +export const { signIn, signUp, signOut } = authClient diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index b9f666c..05ad35c 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -11,4 +11,16 @@ export default defineConfig({ "@": path.resolve(__dirname, "./src"), }, }, + server: { + proxy: { + "/auth": { + target: "http://localhost:4111", + changeOrigin: true, + }, + "/chat": { + target: "http://localhost:4111", + changeOrigin: true, + }, + }, + }, }) diff --git a/package-lock.json b/package-lock.json index de67750..33ae667 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ }, "devDependencies": { "@types/node": "^22.14.1", + "@vitest/coverage-v8": "^3.1.1", "typescript": "^5.8.3", "vitest": "^3.1.1", "yaml": "^2.7.1" @@ -194,6 +195,80 @@ "node": ">=18" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.7", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", @@ -657,6 +732,24 @@ "hono": "^4" } }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@isaacs/ttlcache": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-2.1.4.tgz", @@ -666,6 +759,37 @@ "node": ">=12" } }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -673,6 +797,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@linear/sdk": { "version": "81.0.0", "resolved": "https://registry.npmjs.org/@linear/sdk/-/sdk-81.0.0.tgz", @@ -1007,6 +1142,17 @@ "url": "https://opencollective.com/express" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.60.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", @@ -1683,6 +1829,40 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", @@ -1850,6 +2030,32 @@ } } }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -1875,6 +2081,25 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -1885,6 +2110,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -1929,6 +2164,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -2039,6 +2287,26 @@ "node": ">= 16" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -2213,12 +2481,26 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -2693,6 +2975,23 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2788,6 +3087,61 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2825,6 +3179,16 @@ "node": ">=6.0" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -2880,6 +3244,13 @@ } } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -2967,6 +3338,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-network-error": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.1.tgz", @@ -3027,6 +3408,76 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jose": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", @@ -3137,6 +3588,34 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/markdown-table": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", @@ -3968,6 +4447,32 @@ "node": ">= 0.6" } }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4105,6 +4610,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parse-ms": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", @@ -4135,6 +4647,30 @@ "node": ">=8" } }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/path-to-regexp": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", @@ -4515,6 +5051,19 @@ "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", "license": "BSD-3-Clause" }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", @@ -4736,6 +5285,110 @@ "dev": true, "license": "MIT" }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom-string": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", @@ -4770,6 +5423,19 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/svix": { "version": "1.88.0", "resolved": "https://registry.npmjs.org/svix/-/svix-1.88.0.tgz", @@ -4780,6 +5446,21 @@ "uuid": "^10.0.0" } }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -5282,6 +5963,104 @@ "node": ">=8" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/specs/active/SPEC-20260409-002.md b/specs/active/SPEC-20260409-002.md new file mode 100644 index 0000000..f9b3d92 --- /dev/null +++ b/specs/active/SPEC-20260409-002.md @@ -0,0 +1,314 @@ +# SPEC-20260409-002: Auth Pages (Login/Register) + +**ID:** SPEC-20260409-002 +**Name:** fe-auth-pages-login-register +**Stage:** CODE +**Created:** 2026-04-09 +**Updated:** 2026-04-09 +**Author:** Chenko +**Linear Issue:** FE-03 — Auth Pages (Login/Register) +**Epic:** Frontend +**Tier:** 1 (Must Ship) +**Estimate:** 2h + +## Purpose + +Implement login and registration pages for the Triage app, connecting the frontend to the Better Auth backend (INFRA-02). Users must be able to register, login, and be redirected to the chat interface. Unauthenticated users must be blocked from all app routes. + +## Scope + +### In Scope +- Better Auth client SDK integration in frontend +- `useAuth` hook wired to Better Auth (replacing current stub) +- Login page (`/login`) with email/password form +- Register page (`/register`) with name/email/password form +- Auth guard in root route (redirect unauthenticated → `/login`) +- Post-login/register redirect to `/chat` +- Vite dev proxy for `/auth/*` requests to runtime +- UX spec styling: neumorphic card, navy background, orange CTA + +### Out of Scope +- OAuth/social login providers +- Password reset / forgot password flow +- Email verification flow +- User profile/settings page +- Session refresh/token rotation (backend handles 7-day fixed expiry) +- Sign-out UI (future ticket) + +## Dependencies + +| Dependency | Status | Details | +|-----------|--------|---------| +| INFRA-02 (Better Auth backend) | ✅ Merged to dev, present in this branch | `runtime/src/auth/index.ts`, endpoints at `/auth/*` | +| Better Auth endpoints | ✅ Available | POST `/auth/sign-up/email`, POST `/auth/sign-in/email`, GET `/auth/get-session`, POST `/auth/sign-out` | +| shadcn/ui components | ✅ Available | Card, Input, Button, Label already in `frontend/src/components/ui/` | +| Design system CSS | ✅ Available | Neumorphic shadows, Navy/Orange palette, Space Grotesk/Inter fonts in `index.css` | + +## Requirements + +### REQ-FE01: Better Auth Client SDK Setup +**Priority:** P0 +**Description:** The frontend SHALL have a Better Auth client instance configured to communicate with the backend auth endpoints via the Caddy proxy (same-origin). + +#### Acceptance Criteria +- **GIVEN** the frontend app **WHEN** it initializes **THEN** a Better Auth client is available with `baseURL` pointing to the current origin and `basePath: "/auth"` +- **GIVEN** the auth client **WHEN** any auth request is made **THEN** cookies are sent with `credentials: "include"` + +#### Scenarios +- **Happy path:** Auth client is created at module level, importable from `@/lib/auth-client` +- **Edge case:** Dev mode uses Vite proxy to forward `/auth/*` to `localhost:4111`; production uses Caddy reverse proxy +- **Error case:** If backend is unreachable, auth client methods reject with network error + +#### Implementation +- Create `frontend/src/lib/auth-client.ts` +- Install `better-auth` package in frontend +- Export `authClient` singleton created via `createAuthClient` from `better-auth/react` + +### REQ-FE02: useAuth Hook Implementation +**Priority:** P0 +**Description:** The `useAuth` hook SHALL replace the current stub with a real implementation backed by the Better Auth client SDK, providing reactive session state. + +#### Acceptance Criteria +- **GIVEN** a component using `useAuth()` **WHEN** a session exists **THEN** `isAuthenticated` is `true` and `user` contains `{id, email, name}` +- **GIVEN** a component using `useAuth()` **WHEN** no session exists **THEN** `isAuthenticated` is `false` and `user` is `null` +- **GIVEN** a component using `useAuth()` **WHEN** session check is in progress **THEN** `isLoading` is `true` + +#### Scenarios +- **Happy path:** User is logged in → hook returns user data, `isAuthenticated: true` +- **Edge case:** Session expired (7-day TTL) → `get-session` returns null → `isAuthenticated: false` +- **Error case:** Network failure during session check → `isLoading` resolves to false, `isAuthenticated: false` + +#### Implementation +- Rewrite `frontend/src/hooks/use-auth.ts` +- Use `authClient.useSession()` from Better Auth React integration +- Export additional methods: `signIn`, `signUp`, `signOut` from the auth client +- Maintain existing `AuthState` interface shape for backward compatibility + +### REQ-FE03: Login Page +**Priority:** P0 +**Description:** The app SHALL provide a login page at `/login` with email/password form following the UX design spec. + +#### Acceptance Criteria +- **GIVEN** an unauthenticated user navigates to `/login` **WHEN** the page loads **THEN** a centered card with email and password inputs is displayed on a navy background +- **GIVEN** the login form **WHEN** the user submits valid credentials **THEN** the user is redirected to `/chat` +- **GIVEN** the login form **WHEN** the user submits invalid credentials **THEN** an error message is displayed inline without page reload +- **GIVEN** the login page **WHEN** rendered **THEN** it uses the neumorphic design system (shadow-neu-raised card, shadow-neu-inset inputs, orange CTA button) + +#### Scenarios +- **Happy path:** User enters valid email/password → clicks "Log In" → redirected to `/chat` +- **Edge case:** User is already authenticated → visiting `/login` redirects to `/chat` +- **Error case:** Wrong password → inline error "Invalid email or password", form not cleared +- **Error case:** Empty fields → HTML5 validation prevents submission (required attributes) +- **Error case:** Backend unreachable → error message "Unable to connect. Please try again." + +#### UX Design Spec +- **Background:** `bg-navy` (#1F337A) full viewport +- **Card:** Centered, `shadow-neu-raised`, max-w-sm, rounded-2xl +- **Heading:** "Triage" in Space Grotesk (`font-heading`), large, white +- **Subheading:** One-liner tagline in Inter, muted white/steel-blue +- **Inputs:** Email + Password, `shadow-neu-inset` styling, labels in Inter +- **CTA Button:** "Log In", `bg-orange` (#F28B0D / #E87D08), white text, full-width +- **Footer link:** "Don't have an account? Register" → navigates to `/register` +- **Typography:** Space Grotesk for heading, Inter for everything else + +#### Implementation +- Create `frontend/src/routes/login.tsx` (TanStack Router file-based route) +- Use existing shadcn Card, Input, Button components with neumorphic class overrides +- Call `authClient.signIn.email()` on form submit +- Use `useNavigate()` for post-login redirect to `/chat` + +### REQ-FE04: Register Page +**Priority:** P0 +**Description:** The app SHALL provide a registration page at `/register` with name, email, and password form following the same UX design spec as login. + +#### Acceptance Criteria +- **GIVEN** an unauthenticated user navigates to `/register` **WHEN** the page loads **THEN** a centered card with name, email, and password inputs is displayed on a navy background +- **GIVEN** the register form **WHEN** the user submits valid data **THEN** an account is created and the user is automatically logged in and redirected to `/chat` +- **GIVEN** the register form **WHEN** the email is already registered **THEN** an error message is displayed inline +- **GIVEN** the register page **WHEN** rendered **THEN** it matches the neumorphic styling of the login page + +#### Scenarios +- **Happy path:** User fills name/email/password → clicks "Create Account" → account created → redirected to `/chat` +- **Edge case:** User is already authenticated → visiting `/register` redirects to `/chat` +- **Error case:** Duplicate email → inline error "An account with this email already exists" +- **Error case:** Weak password (if backend enforces) → inline error with backend message +- **Error case:** Backend unreachable → error message "Unable to connect. Please try again." + +#### UX Design Spec +- Same visual style as login page (navy bg, neumorphic card, orange CTA) +- **CTA Button:** "Create Account" instead of "Log In" +- **Footer link:** "Already have an account? Log in" → navigates to `/login` +- **Extra field:** "Name" input above email + +#### Implementation +- Create `frontend/src/routes/register.tsx` (TanStack Router file-based route) +- Call `authClient.signUp.email()` with `{ name, email, password }` +- Auto-login after signup by calling `signIn.email()` or relying on Better Auth's auto-session +- Use `useNavigate()` for redirect to `/chat` + +### REQ-FE05: Auth Guard (Protected Routes) +**Priority:** P0 +**Description:** The root route SHALL enforce authentication by redirecting unauthenticated users to `/login`, while allowing access to auth pages without a session. + +#### Acceptance Criteria +- **GIVEN** an unauthenticated user **WHEN** they navigate to any route except `/login` or `/register` **THEN** they are redirected to `/login` +- **GIVEN** an authenticated user **WHEN** they navigate to `/login` or `/register` **THEN** they are redirected to `/chat` +- **GIVEN** the auth state is loading **WHEN** any page renders **THEN** a loading spinner is shown (not a flash of login page) + +#### Scenarios +- **Happy path:** Authenticated user navigates freely between `/chat`, `/board`, `/settings` +- **Edge case:** Direct URL access to `/chat` while unauthenticated → redirect to `/login` +- **Edge case:** Browser refresh while authenticated → loading spinner → content (no flash) +- **Error case:** Session check fails (network) → redirect to `/login` as fallback + +#### Implementation +- Uncomment and activate the auth guard in `frontend/src/routes/__root.tsx` (lines 20-31) +- Add `Navigate` import from `@tanstack/react-router` +- Add authenticated-user redirect away from auth pages +- Update the sidebar user info section to show real user data from `useAuth()` + +### REQ-FE06: Vite Dev Proxy Configuration +**Priority:** P1 +**Description:** The Vite dev server SHALL proxy `/auth/*` requests to the Mastra runtime so auth works in local development without Caddy. + +#### Acceptance Criteria +- **GIVEN** the frontend runs via `vite dev` **WHEN** a request to `/auth/*` is made **THEN** it is proxied to `http://localhost:4111/auth/*` +- **GIVEN** the proxy **WHEN** forwarding requests **THEN** cookies and headers are preserved + +#### Scenarios +- **Happy path:** `POST /auth/sign-in/email` from Vite dev → proxied to runtime:4111 → session cookie set +- **Edge case:** Runtime is not running → proxy returns 502, frontend shows connection error +- **Error case:** CORS issues avoided because proxy makes it same-origin + +#### Implementation +- Add `server.proxy` config in `frontend/vite.config.ts` for `/auth` → `http://localhost:4111` +- Also proxy `/chat` endpoint (already needed for AI SDK chat transport) + +## Technical Approach + +### Architecture +``` +Browser → Vite dev (:5173) → proxy /auth/* → Mastra runtime (:4111) → Better Auth → LibSQL +Browser → Caddy (:3001) → /auth/* → Mastra runtime (:4111) → Better Auth → LibSQL +``` + +### Better Auth Client Setup +```typescript +// frontend/src/lib/auth-client.ts +import { createAuthClient } from "better-auth/react" + +export const authClient = createAuthClient({ + baseURL: window.location.origin, // same-origin via proxy/Caddy + basePath: "/auth", +}) +``` + +### Key Technical Decisions +1. **Same-origin auth** — No CORS needed. Vite proxy in dev, Caddy in prod. Auth client uses `window.location.origin`. +2. **HttpOnly cookies** — Frontend cannot read session token. Must call `GET /auth/get-session` to check auth state. +3. **Better Auth React integration** — `authClient.useSession()` provides reactive hook with auto-refetch. +4. **TanStack Router file-based routing** — Login/Register are file routes at `routes/login.tsx` and `routes/register.tsx`. +5. **No additional state management** — Better Auth client handles session caching internally. + +### Files to Create/Modify + +| File | Action | Owner (Subagent) | +|------|--------|-------------------| +| `frontend/src/lib/auth-client.ts` | CREATE | Agent A | +| `frontend/src/hooks/use-auth.ts` | REWRITE | Agent A | +| `frontend/vite.config.ts` | MODIFY (add proxy) | Agent A | +| `frontend/src/routes/login.tsx` | CREATE | Agent B | +| `frontend/src/routes/register.tsx` | CREATE | Agent C | +| `frontend/src/routes/__root.tsx` | MODIFY (activate guard) | Agent C | + +## Test Strategy + +### Unit Tests (vitest + @testing-library/react) +- `useAuth` hook returns correct states (loading, authenticated, unauthenticated) +- Login form validates required fields +- Register form validates required fields +- Auth guard redirects correctly based on auth state + +### Integration Tests +- Login flow: submit form → API call → redirect +- Register flow: submit form → API call → auto-login → redirect +- Protected route redirect when unauthenticated + +### Coverage Target +- All auth client functions covered +- All form submission paths covered +- All error states covered + +## Implementation Plan — Parallel Subagent Strategy + +This spec SHALL be implemented using 3 parallel Hermes subagents (model: `claude-sonnet-4-6`) to maximize speed within the 2h estimate. + +### Subagent Architecture + +``` +Orchestrator (this session) +├── Agent A: Auth Foundation [REQ-FE01, REQ-FE02, REQ-FE06] +├── Agent B: Login Page [REQ-FE03] +└── Agent C: Register + Guard [REQ-FE04, REQ-FE05] + (all run in parallel) +``` + +### Agent A — Auth Foundation +**Scope:** REQ-FE01, REQ-FE02, REQ-FE06 +**Files owned:** +- CREATE `frontend/src/lib/auth-client.ts` +- REWRITE `frontend/src/hooks/use-auth.ts` +- MODIFY `frontend/vite.config.ts` (add proxy for `/auth` and `/chat`) + +**Context to inject:** +- Backend auth config from `runtime/src/auth/index.ts` (basePath: '/auth', emailAndPassword enabled) +- Available endpoints: POST `/auth/sign-up/email`, POST `/auth/sign-in/email`, GET `/auth/get-session`, POST `/auth/sign-out` +- Current stub shape in `use-auth.ts` (must maintain `AuthState` interface) +- Better Auth React client: `createAuthClient` from `better-auth/react`, `authClient.useSession()` returns `{ data: session, isPending, error }` +- Install `better-auth` in frontend via `cd frontend && npm install better-auth` + +### Agent B — Login Page +**Scope:** REQ-FE03 +**Files owned:** +- CREATE `frontend/src/routes/login.tsx` + +**Context to inject:** +- TanStack Router file-based routing: `createFileRoute("/login")` pattern (see existing `chat.tsx`) +- Auth client interface: `import { authClient } from "@/lib/auth-client"`, call `authClient.signIn.email({ email, password })` +- Available components: `Card, CardHeader, CardContent, CardFooter` from `@/components/ui/card`, `Input` from `@/components/ui/input`, `Button` from `@/components/ui/button` +- UX spec: navy bg (#1F337A → `bg-navy`), neumorphic card (`shadow-neu-raised`), inset inputs (`shadow-neu-inset`), orange CTA (`bg-orange`), heading in `font-heading` (Space Grotesk), body in `font-sans` (Inter) +- Post-login: `useNavigate()` → `/chat` +- Link to `/register` in footer +- CSS vars already defined: `--color-navy`, `--color-orange`, `--shadow-neu-raised`, `--shadow-neu-inset` + +### Agent C — Register Page + Auth Guard +**Scope:** REQ-FE04, REQ-FE05 +**Files owned:** +- CREATE `frontend/src/routes/register.tsx` +- MODIFY `frontend/src/routes/__root.tsx` (activate auth guard) + +**Context to inject:** +- Same UX spec and component context as Agent B +- Auth client: `authClient.signUp.email({ name, email, password })` for registration +- Current `__root.tsx` content (full file) — uncomment lines 20-31, add `Navigate` import, add redirect-away from auth pages if authenticated +- Sidebar user info: replace hardcoded "K" / "Koki" / "Developer" with `user.name` from `useAuth()` +- Post-register: auto-login + redirect to `/chat` + +### Execution Order +1. **Parallel dispatch:** All 3 agents launch simultaneously via `delegate_task` with `acp_command: "claude"` and `acp_args: ["--acp", "--stdio", "--model", "claude-sonnet-4-6"]` +2. **Orchestrator waits** for all agents to complete +3. **Orchestrator verifies:** typecheck (`cd frontend && npx tsc --noEmit`), route generation (`npx tanstack-router generate`) +4. **Fix pass:** If typecheck fails, orchestrator applies targeted patches +5. **Commit:** Single commit with all changes + +### Subagent Toolsets +- All agents: `["terminal", "file"]` — no web access needed, all context injected in goal +- Working directory: `~/hackathon/triage-feature-fe-auth-pages` + +### Key Constraints for Subagents +- Do NOT modify files owned by other agents +- Use existing shadcn/ui components — do NOT install new UI libraries +- Use existing CSS variables and design tokens — do NOT create new theme files +- Better Auth React client uses `createAuthClient` from `"better-auth/react"` (NOT `"better-auth/client"`) +- Form state management: plain React `useState` — no form libraries +- All imports use `@/` alias (mapped to `frontend/src/` via vite.config.ts) diff --git a/tests/fe-auth-pages/auth-client.test.ts b/tests/fe-auth-pages/auth-client.test.ts new file mode 100644 index 0000000..930f6f6 --- /dev/null +++ b/tests/fe-auth-pages/auth-client.test.ts @@ -0,0 +1,147 @@ +/** + * SPEC-20260409-002 — Auth Pages (Login/Register) + * REQ-FE01: Better Auth Client SDK Setup + * REQ-FE02: useAuth Hook Implementation + * + * Tests for auth client configuration and useAuth hook behavior. + */ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'fs'; +import { resolve } from 'path'; + +const PROJECT_ROOT = resolve(__dirname, '../../'); + +function readFile(relativePath: string): string { + return readFileSync(resolve(PROJECT_ROOT, relativePath), 'utf-8'); +} + +describe('SPEC-20260409-002: Auth Pages — Auth Foundation', () => { + + // ─── REQ-FE01: Better Auth Client SDK Setup ───────────────────── + + describe('REQ-FE01: Better Auth Client SDK Setup', () => { + + it('should export an auth client configured with baseURL as current origin', () => { + // Verify: auth-client.ts exports authClient with baseURL === window.location.origin + const src = readFile('frontend/src/lib/auth-client.ts'); + expect(src).toContain('export const authClient'); + expect(src).toContain('createAuthClient'); + expect(src).toContain('window.location.origin'); + }); + + it('should configure auth client with basePath "/auth"', () => { + // Verify: authClient config has basePath: "/auth" + const src = readFile('frontend/src/lib/auth-client.ts'); + expect(src).toMatch(/basePath:\s*["']\/?auth["']/); + }); + + it('should send cookies with credentials include on auth requests', () => { + // Better Auth react client includes credentials by default. + // Verify the client is created via createAuthClient from better-auth/react + const src = readFile('frontend/src/lib/auth-client.ts'); + expect(src).toContain('createAuthClient'); + expect(src).toContain('better-auth/react'); + // better-auth/react createAuthClient sends cookies with credentials: "include" by default + }); + + it('should be importable from @/lib/auth-client', () => { + // Happy path: Auth client is created at module level, importable from @/lib/auth-client + const src = readFile('frontend/src/lib/auth-client.ts'); + expect(src).toContain('export const authClient'); + // Verify the file physically exists + const fullPath = resolve(PROJECT_ROOT, 'frontend/src/lib/auth-client.ts'); + expect(() => readFileSync(fullPath)).not.toThrow(); + }); + + it('should handle dev mode with Vite proxy forwarding /auth/* to localhost:4111', () => { + // Verify: Vite config contains proxy entry for /auth + const viteConfig = readFile('frontend/vite.config.ts'); + expect(viteConfig).toContain('proxy'); + expect(viteConfig).toMatch(/["']\/auth["']/); + expect(viteConfig).toContain('localhost:4111'); + }); + + it('should reject with network error if backend is unreachable', () => { + // Error case: signIn, signUp, signOut are exported so they can be called + // When the backend is unreachable, fetch-based calls will reject with a network error. + // Verify the methods are exported from auth-client. + const src = readFile('frontend/src/lib/auth-client.ts'); + expect(src).toContain('export const { signIn, signUp, signOut }'); + // The network error handling is built into better-auth's fetch client + }); + }); + + // ─── REQ-FE02: useAuth Hook Implementation ────────────────────── + + describe('REQ-FE02: useAuth Hook Implementation', () => { + + const hookSrc = readFile('frontend/src/hooks/use-auth.ts'); + + it('should return isAuthenticated true and user data when session exists', () => { + // Verify: hook checks session.data?.user and returns isAuthenticated: !!session.data?.user + expect(hookSrc).toContain('isAuthenticated'); + expect(hookSrc).toMatch(/isAuthenticated:\s*!!session\.data\?\.user/); + }); + + it('should return isAuthenticated false and user null when no session exists', () => { + // Verify: when session.data?.user is falsy, user is null and isAuthenticated is false + // The ternary checks session.data?.user — when null, user is null + expect(hookSrc).toMatch(/session\.data\?\.user\s*\n?\s*\?/); + expect(hookSrc).toContain(': null'); + }); + + it('should return isLoading true while session check is in progress', () => { + // Verify: hook maps session.isPending to isLoading + expect(hookSrc).toContain('isPending'); + expect(hookSrc).toMatch(/isLoading:\s*session\.isPending/); + }); + + it('should return user data with correct shape when logged in', () => { + // Verify: user object has id, email, name fields mapped from session + expect(hookSrc).toContain('session.data.user.id'); + expect(hookSrc).toContain('session.data.user.email'); + expect(hookSrc).toContain('session.data.user.name'); + }); + + it('should handle expired session by returning isAuthenticated false', () => { + // Edge case: Session expired → get-session returns null → session.data?.user is falsy + // Verify: hook uses optional chaining on session.data + expect(hookSrc).toContain('session.data?.user'); + // When expired, session.data is null, so isAuthenticated becomes false + expect(hookSrc).toMatch(/isAuthenticated:\s*!!session\.data\?\.user/); + }); + + it('should resolve to unauthenticated on network failure during session check', () => { + // Error case: Network failure during session check → session.data is null + // useSession() from better-auth handles errors internally; isPending resolves to false + // Verify: hook relies on session.data?.user which is null on error + expect(hookSrc).toContain('authClient.useSession()'); + expect(hookSrc).toContain('session.data?.user'); + }); + + it('should export signIn, signUp, signOut methods from auth client', () => { + // Verify: useAuth or auth-client exports signIn, signUp, signOut functions + expect(hookSrc).toContain('signIn'); + expect(hookSrc).toContain('signUp'); + expect(hookSrc).toContain('signOut'); + // Also verify auth-client.ts exports them + const authClientSrc = readFile('frontend/src/lib/auth-client.ts'); + expect(authClientSrc).toContain('signIn'); + expect(authClientSrc).toContain('signUp'); + expect(authClientSrc).toContain('signOut'); + }); + + it('should maintain backward-compatible AuthState interface shape', () => { + // Verify: hook returns object matching { user: User | null, isLoading: boolean, isAuthenticated: boolean } + expect(hookSrc).toContain('export interface AuthState'); + expect(hookSrc).toMatch(/user:\s*User\s*\|\s*null/); + expect(hookSrc).toMatch(/isLoading:\s*boolean/); + expect(hookSrc).toMatch(/isAuthenticated:\s*boolean/); + // Also verify User interface + expect(hookSrc).toContain('export interface User'); + expect(hookSrc).toContain('id: string'); + expect(hookSrc).toContain('email: string'); + expect(hookSrc).toContain('name: string'); + }); + }); +}); diff --git a/tests/fe-auth-pages/vite-proxy.test.ts b/tests/fe-auth-pages/vite-proxy.test.ts new file mode 100644 index 0000000..a34f754 --- /dev/null +++ b/tests/fe-auth-pages/vite-proxy.test.ts @@ -0,0 +1,77 @@ +/** + * SPEC-20260409-002 — Auth Pages (Login/Register) + * REQ-FE06: Vite Dev Proxy Configuration + * + * Tests for Vite proxy setup — /auth/* and /chat forwarding to runtime in dev mode. + */ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'fs'; +import { resolve } from 'path'; + +const PROJECT_ROOT = resolve(__dirname, '../../'); + +function readViteConfig(): string { + return readFileSync(resolve(PROJECT_ROOT, 'frontend/vite.config.ts'), 'utf-8'); +} + +describe('SPEC-20260409-002: Auth Pages — Vite Dev Proxy', () => { + + // ─── REQ-FE06: Vite Dev Proxy Configuration ──────────────────── + + describe('REQ-FE06: Vite Dev Proxy Configuration', () => { + + const viteConfig = readViteConfig(); + + // Acceptance Criteria + it('should proxy /auth/* requests to http://localhost:4111 in Vite dev', () => { + // GIVEN the frontend runs via vite dev + // WHEN a request to /auth/* is made + // THEN it is proxied to http://localhost:4111/auth/* + // Verify: vite.config.ts contains server.proxy entry for /auth → localhost:4111 + expect(viteConfig).toContain('server'); + expect(viteConfig).toContain('proxy'); + expect(viteConfig).toMatch(/["']\/auth["']/); + expect(viteConfig).toContain('http://localhost:4111'); + }); + + it('should preserve cookies and headers when proxying', () => { + // GIVEN the proxy + // WHEN forwarding requests + // THEN cookies and headers are preserved + // Verify: proxy config includes changeOrigin and cookie handling + expect(viteConfig).toContain('changeOrigin'); + expect(viteConfig).toMatch(/changeOrigin:\s*true/); + }); + + // Scenarios — Happy path + it('should forward POST /auth/sign-in/email to runtime:4111', () => { + // Happy path: POST /auth/sign-in/email from Vite dev → proxied to runtime:4111 → session cookie set + // Verify: proxy target is http://localhost:4111, path /auth is matched + expect(viteConfig).toMatch(/["']\/auth["']\s*:\s*\{/); + expect(viteConfig).toContain('target'); + expect(viteConfig).toContain('http://localhost:4111'); + }); + + // Scenarios — Edge case + it('should also proxy /chat endpoint for AI SDK chat transport', () => { + // Edge case: /chat endpoint also needs proxy for AI SDK + // Verify: vite.config.ts has proxy entry for /chat → localhost:4111 + expect(viteConfig).toMatch(/["']\/chat["']/); + expect(viteConfig).toMatch(/["']\/chat["']\s*:\s*\{/); + // Verify it also targets localhost:4111 + // Both /auth and /chat should point to same target + const chatSection = viteConfig.split('/chat')[1]; + expect(chatSection).toContain('localhost:4111'); + }); + + // Scenarios — Error case + it('should return 502 when runtime is not running', () => { + // Error case: Runtime is not running → proxy returns 502, frontend shows connection error + // Verify: proxy config exists (502 is handled by Vite automatically when target is unreachable) + expect(viteConfig).toContain('proxy'); + expect(viteConfig).toMatch(/["']\/auth["']/); + expect(viteConfig).toMatch(/["']\/chat["']/); + // Vite's built-in proxy (http-proxy) automatically returns 502 when the target is down + }); + }); +}); From 2ba6beebf5594a94c8187c4e7312c47e49e40888 Mon Sep 17 00:00:00 2001 From: luci-efe Date: Thu, 9 Apr 2026 06:38:23 -0600 Subject: [PATCH 02/11] =?UTF-8?q?feat(auth):=20register=20page=20+=20auth?= =?UTF-8?q?=20guard=20activation=20=E2=80=94=20SPEC-20260409-002=20(Agent?= =?UTF-8?q?=20C:=2012/12=20register=20tests)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/routes/__root.tsx | 37 ++-- frontend/src/routes/register.tsx | 149 ++++++++++++++++ tests/fe-auth-pages/register-page.test.ts | 199 ++++++++++++++++++++++ 3 files changed, 368 insertions(+), 17 deletions(-) create mode 100644 frontend/src/routes/register.tsx create mode 100644 tests/fe-auth-pages/register-page.test.ts diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx index dcfb51b..4d3da9e 100644 --- a/frontend/src/routes/__root.tsx +++ b/frontend/src/routes/__root.tsx @@ -1,4 +1,4 @@ -import { createRootRoute, Link, Outlet, useLocation } from "@tanstack/react-router" +import { createRootRoute, Link, Navigate, Outlet, useLocation } from "@tanstack/react-router" import { TanStackRouterDevtools } from "@tanstack/router-devtools" import { MessageSquare, LayoutGrid, Settings, Sun, Moon } from "lucide-react" import { useAuth } from "@/hooks/use-auth" @@ -9,7 +9,7 @@ export const Route = createRootRoute({ }) function RootLayout() { - const { /* isAuthenticated, isLoading */ } = useAuth() + const { isAuthenticated, isLoading, user } = useAuth() const { resolvedTheme, setTheme } = useTheme() const location = useLocation() @@ -17,18 +17,21 @@ function RootLayout() { const isAuthPage = location.pathname === "/login" || location.pathname === "/register" - // TODO: Uncomment when Better Auth is connected (TRI-4 + TRI-21) - // if (isLoading) { - // return ( - //
- //
- //
- // ) - // } - // - // if (!isAuthenticated && !isAuthPage) { - // return - // } + if (isLoading) { + return ( +
+
+
+ ) + } + + if (!isAuthenticated && !isAuthPage) { + return + } + + if (isAuthenticated && isAuthPage) { + return + } // Auth pages render without sidebar if (isAuthPage) { @@ -83,11 +86,11 @@ function RootLayout() {
- K + {user?.name?.[0]?.toUpperCase() || "?"}
-

Koki

-

Developer

+

{user?.name || "User"}

+

{user?.email || ""}

diff --git a/frontend/src/routes/register.tsx b/frontend/src/routes/register.tsx new file mode 100644 index 0000000..06584e2 --- /dev/null +++ b/frontend/src/routes/register.tsx @@ -0,0 +1,149 @@ +import { createFileRoute, Link, useNavigate } from "@tanstack/react-router" +import { useState } from "react" +import { useAuth } from "@/hooks/use-auth" +import { authClient } from "@/lib/auth-client" + +export const Route = createFileRoute("/register")({ + component: RegisterPage, +}) + +function RegisterPage() { + const { isAuthenticated } = useAuth() + const navigate = useNavigate() + const [name, setName] = useState("") + const [email, setEmail] = useState("") + const [password, setPassword] = useState("") + const [error, setError] = useState(null) + const [isSubmitting, setIsSubmitting] = useState(false) + + // Redirect authenticated users to /chat + if (isAuthenticated) { + navigate({ to: "/chat" }) + return null + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError(null) + setIsSubmitting(true) + + try { + const result = await authClient.signUp.email({ + name, + email, + password, + }) + + if (result.error) { + if (result.error.message?.toLowerCase().includes("already") || result.error.code === "USER_ALREADY_EXISTS") { + setError("An account with this email already exists") + } else if (result.error.message?.toLowerCase().includes("password")) { + setError(result.error.message) + } else { + setError(result.error.message || "Registration failed. Please try again.") + } + } else { + navigate({ to: "/chat" }) + } + } catch (err) { + setError("Unable to connect. Please try again.") + } finally { + setIsSubmitting(false) + } + } + + return ( +
+
+
+

+ Triage +

+

+ Create your account +

+
+ +
+
+ + setName(e.target.value)} + placeholder="Your name" + className="w-full rounded-xl border-0 bg-muted/50 px-4 py-2.5 text-sm text-foreground shadow-neu-inset outline-none placeholder:text-muted-foreground/50 focus:ring-2 focus:ring-primary/30" + /> +
+ +
+ + setEmail(e.target.value)} + placeholder="you@example.com" + className="w-full rounded-xl border-0 bg-muted/50 px-4 py-2.5 text-sm text-foreground shadow-neu-inset outline-none placeholder:text-muted-foreground/50 focus:ring-2 focus:ring-primary/30" + /> +
+ +
+ + setPassword(e.target.value)} + placeholder="••••••••" + className="w-full rounded-xl border-0 bg-muted/50 px-4 py-2.5 text-sm text-foreground shadow-neu-inset outline-none placeholder:text-muted-foreground/50 focus:ring-2 focus:ring-primary/30" + /> +
+ + {error && ( +
+ {error} +
+ )} + + +
+ +

+ Already have an account?{" "} + + Log in + +

+
+
+ ) +} diff --git a/tests/fe-auth-pages/register-page.test.ts b/tests/fe-auth-pages/register-page.test.ts new file mode 100644 index 0000000..9306db9 --- /dev/null +++ b/tests/fe-auth-pages/register-page.test.ts @@ -0,0 +1,199 @@ +/** + * SPEC-20260409-002 — Auth Pages (Login/Register) + * REQ-FE04: Register Page + * + * Tests for the /register route — form rendering, submission, auto-login, error handling. + * Validates source code structure and correctness via file inspection. + */ +import { describe, it, expect } from 'vitest'; +import { readFileSync, existsSync } from 'fs'; +import { resolve } from 'path'; + +const PROJECT_ROOT = resolve(__dirname, '../../'); +const REGISTER_PAGE = resolve(PROJECT_ROOT, 'frontend/src/routes/register.tsx'); + +function readRegisterPage(): string { + expect(existsSync(REGISTER_PAGE), 'register.tsx must exist').toBe(true); + return readFileSync(REGISTER_PAGE, 'utf-8'); +} + +describe('SPEC-20260409-002: Auth Pages — Register Page', () => { + + // ─── REQ-FE04: Register Page ──────────────────────────────────── + + describe('REQ-FE04: Register Page', () => { + + // Acceptance Criteria + it('should display centered card with name, email, and password inputs on navy background', () => { + // GIVEN an unauthenticated user navigates to /register + // WHEN the page loads + // THEN a centered card with name, email, and password inputs is displayed on a navy background + const src = readRegisterPage(); + + // Navy background on container + expect(src).toContain('bg-navy'); + + // Centered layout + expect(src).toMatch(/items-center/); + expect(src).toMatch(/justify-center/); + + // Card element + expect(src).toContain('bg-card'); + + // Three inputs: name, email, password + expect(src).toMatch(/type="text"/); + expect(src).toMatch(/type="email"/); + expect(src).toMatch(/type="password"/); + + // Input IDs + expect(src).toMatch(/id="name"/); + expect(src).toMatch(/id="email"/); + expect(src).toMatch(/id="password"/); + }); + + it('should create account, auto-login, and redirect to /chat on valid submission', () => { + // GIVEN the register form + // WHEN the user submits valid data + // THEN an account is created and the user is automatically logged in and redirected to /chat + const src = readRegisterPage(); + + // Calls signUp.email + expect(src).toMatch(/authClient\.signUp\.email/); + + // Passes name, email, password + expect(src).toMatch(/signUp\.email\(\s*\{/); + expect(src).toContain('name'); + expect(src).toContain('email'); + expect(src).toContain('password'); + + // Navigates to /chat on success + expect(src).toMatch(/navigate\(\s*\{\s*to:\s*["']\/chat["']/); + }); + + it('should display inline error when email is already registered', () => { + // GIVEN the register form + // WHEN the email is already registered + // THEN an error message is displayed inline + const src = readRegisterPage(); + + // Handles duplicate email error + expect(src).toMatch(/already/i); + expect(src).toMatch(/error/i); + + // Error is shown inline (role="alert" or error state rendered) + expect(src).toMatch(/role="alert"|error.*&&/i); + }); + + it('should match neumorphic styling of the login page', () => { + // GIVEN the register page + // WHEN rendered + // THEN it matches the neumorphic styling of the login page + const src = readRegisterPage(); + + // shadow-neu-raised on card + expect(src).toContain('shadow-neu-raised'); + + // shadow-neu-inset on inputs + expect(src).toContain('shadow-neu-inset'); + + // bg-orange on CTA button + expect(src).toContain('bg-orange'); + }); + + // Scenarios — Happy path + it('should complete register flow: fill name/email/password → click Create Account → redirect to /chat', () => { + // Happy path: User fills name/email/password → clicks "Create Account" → account created → redirected to /chat + const src = readRegisterPage(); + + // Form has onSubmit handler + expect(src).toMatch(/onSubmit=\{handleSubmit\}/); + + // signUp.email called with { name, email, password } + expect(src).toMatch(/authClient\.signUp\.email\(\s*\{/); + + // Navigates to /chat + expect(src).toMatch(/navigate\(\s*\{\s*to:\s*["']\/chat["']/); + + // Uses state for form fields + expect(src).toMatch(/useState.*""/); + }); + + // Scenarios — Edge case + it('should redirect already authenticated user from /register to /chat', () => { + // Edge case: User is already authenticated → visiting /register redirects to /chat + const src = readRegisterPage(); + + // Checks isAuthenticated + expect(src).toMatch(/isAuthenticated/); + + // Redirects to /chat + expect(src).toMatch(/if\s*\(\s*isAuthenticated\s*\)/); + expect(src).toMatch(/navigate\(\s*\{\s*to:\s*["']\/chat["']/); + }); + + // Scenarios — Error cases + it('should show "An account with this email already exists" on duplicate email', () => { + // Error case: Duplicate email → inline error "An account with this email already exists" + const src = readRegisterPage(); + + expect(src).toContain('An account with this email already exists'); + }); + + it('should show inline error on weak password if backend enforces', () => { + // Error case: Weak password (if backend enforces) → inline error with backend message + const src = readRegisterPage(); + + // Handles password-related errors from backend + expect(src).toMatch(/password/i); + + // Uses result.error.message for display + expect(src).toMatch(/result\.error\.message/); + + // Error state displayed inline + expect(src).toMatch(/\{error\s*&&/); + }); + + it('should show connection error when backend is unreachable', () => { + // Error case: Backend unreachable → error message "Unable to connect. Please try again." + const src = readRegisterPage(); + + expect(src).toContain('Unable to connect. Please try again.'); + + // Uses try/catch for network errors + expect(src).toMatch(/catch\s*\(/); + }); + + // UX Design elements + it('should render "Create Account" CTA button with orange background', () => { + // Verify: submit button text is "Create Account", has bg-orange class + const src = readRegisterPage(); + + expect(src).toContain('Create Account'); + expect(src).toContain('bg-orange'); + + // Button is type="submit" + expect(src).toMatch(/type="submit"/); + }); + + it('should render footer link to /login', () => { + // Verify: link with text containing "Log in" points to /login + const src = readRegisterPage(); + + expect(src).toContain('Log in'); + expect(src).toMatch(/to="\/login"/); + expect(src).toContain('Already have an account?'); + }); + + it('should render name input above email input', () => { + // Verify: name input appears before email input in DOM order + const src = readRegisterPage(); + + const namePos = src.indexOf('id="name"'); + const emailPos = src.indexOf('id="email"'); + + expect(namePos).toBeGreaterThan(-1); + expect(emailPos).toBeGreaterThan(-1); + expect(namePos).toBeLessThan(emailPos); + }); + }); +}); From f9facf09cc037a9788c1b265a5605b7e9c1da5f1 Mon Sep 17 00:00:00 2001 From: luci-efe Date: Thu, 9 Apr 2026 06:41:15 -0600 Subject: [PATCH 03/11] =?UTF-8?q?feat(auth):=20login=20page=20with=20neumo?= =?UTF-8?q?rphic=20UX=20=E2=80=94=20SPEC-20260409-002=20(13/13=20tests)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/routes/login.tsx | 123 +++++++++++++++ tests/fe-auth-pages/login-page.test.ts | 200 +++++++++++++++++++++++++ 2 files changed, 323 insertions(+) create mode 100644 frontend/src/routes/login.tsx create mode 100644 tests/fe-auth-pages/login-page.test.ts diff --git a/frontend/src/routes/login.tsx b/frontend/src/routes/login.tsx new file mode 100644 index 0000000..330913c --- /dev/null +++ b/frontend/src/routes/login.tsx @@ -0,0 +1,123 @@ +import { createFileRoute, Link, useNavigate } from "@tanstack/react-router" +import { useState } from "react" +import { useAuth } from "@/hooks/use-auth" +import { authClient } from "@/lib/auth-client" + +export const Route = createFileRoute("/login")({ + component: LoginPage, +}) + +function LoginPage() { + const { isAuthenticated } = useAuth() + const navigate = useNavigate() + const [email, setEmail] = useState("") + const [password, setPassword] = useState("") + const [error, setError] = useState(null) + const [isSubmitting, setIsSubmitting] = useState(false) + + // Redirect authenticated users to /chat + if (isAuthenticated) { + navigate({ to: "/chat" }) + return null + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError(null) + setIsSubmitting(true) + + try { + const result = await authClient.signIn.email({ + email, + password, + }) + + if (result.error) { + setError(result.error.message || "Invalid email or password") + } else { + navigate({ to: "/chat" }) + } + } catch (err) { + setError("Unable to connect. Please try again.") + } finally { + setIsSubmitting(false) + } + } + + return ( +
+
+
+

+ Triage +

+

+ SRE Intelligence Platform +

+
+ +
+
+ + setEmail(e.target.value)} + placeholder="you@example.com" + className="w-full rounded-xl border-0 bg-muted/50 px-4 py-2.5 text-sm text-foreground shadow-neu-inset outline-none placeholder:text-muted-foreground/50 focus:ring-2 focus:ring-primary/30" + /> +
+ +
+ + setPassword(e.target.value)} + placeholder="••••••••" + className="w-full rounded-xl border-0 bg-muted/50 px-4 py-2.5 text-sm text-foreground shadow-neu-inset outline-none placeholder:text-muted-foreground/50 focus:ring-2 focus:ring-primary/30" + /> +
+ + {error && ( +
+ {error} +
+ )} + + +
+ +

+ Don't have an account?{" "} + + Register + +

+
+
+ ) +} diff --git a/tests/fe-auth-pages/login-page.test.ts b/tests/fe-auth-pages/login-page.test.ts new file mode 100644 index 0000000..9536725 --- /dev/null +++ b/tests/fe-auth-pages/login-page.test.ts @@ -0,0 +1,200 @@ +/** + * SPEC-20260409-002 — Auth Pages (Login/Register) + * REQ-FE03: Login Page + * + * Tests for the /login route — form rendering, submission, error handling, and UX styling. + * Validates source code structure and correctness via file inspection. + */ +import { describe, it, expect } from 'vitest'; +import { readFileSync, existsSync } from 'fs'; +import { resolve } from 'path'; + +const PROJECT_ROOT = resolve(__dirname, '../../'); +const LOGIN_PAGE = resolve(PROJECT_ROOT, 'frontend/src/routes/login.tsx'); + +function readLoginPage(): string { + expect(existsSync(LOGIN_PAGE), 'login.tsx must exist').toBe(true); + return readFileSync(LOGIN_PAGE, 'utf-8'); +} + +describe('SPEC-20260409-002: Auth Pages — Login Page', () => { + + // ─── REQ-FE03: Login Page ─────────────────────────────────────── + + describe('REQ-FE03: Login Page', () => { + + // Acceptance Criteria + it('should display centered card with email and password inputs on navy background', () => { + // GIVEN an unauthenticated user navigates to /login + // WHEN the page loads + // THEN a centered card with email and password inputs is displayed on a navy background + const src = readLoginPage(); + + // Navy background on container + expect(src).toContain('bg-navy'); + + // Centered layout + expect(src).toMatch(/items-center/); + expect(src).toMatch(/justify-center/); + + // Card element + expect(src).toContain('bg-card'); + + // Two inputs: email, password (no name/text input) + expect(src).toMatch(/type="email"/); + expect(src).toMatch(/type="password"/); + + // Input IDs + expect(src).toMatch(/id="email"/); + expect(src).toMatch(/id="password"/); + }); + + it('should redirect to /chat on successful login', () => { + // GIVEN the login form + // WHEN the user submits valid credentials + // THEN the user is redirected to /chat + const src = readLoginPage(); + + // Calls signIn.email + expect(src).toMatch(/authClient\.signIn\.email/); + + // Passes email, password + expect(src).toMatch(/signIn\.email\(\s*\{/); + expect(src).toContain('email'); + expect(src).toContain('password'); + + // Navigates to /chat on success + expect(src).toMatch(/navigate\(\s*\{\s*to:\s*["']\/chat["']/); + }); + + it('should display inline error on invalid credentials without page reload', () => { + // GIVEN the login form + // WHEN the user submits invalid credentials + // THEN an error message is displayed inline without page reload + const src = readLoginPage(); + + // Handles error from result + expect(src).toMatch(/result\.error/); + expect(src).toMatch(/setError/); + + // Error is shown inline (role="alert" or error state rendered) + expect(src).toMatch(/role="alert"|error.*&&/i); + }); + + it('should use neumorphic design system styling', () => { + // GIVEN the login page + // WHEN rendered + // THEN it uses shadow-neu-raised card, shadow-neu-inset inputs, orange CTA button + const src = readLoginPage(); + + // shadow-neu-raised on card + expect(src).toContain('shadow-neu-raised'); + + // shadow-neu-inset on inputs + expect(src).toContain('shadow-neu-inset'); + + // bg-orange on CTA button + expect(src).toContain('bg-orange'); + }); + + // Scenarios — Happy path + it('should complete login flow: enter email/password → click Log In → redirect to /chat', () => { + // Happy path: User enters valid email/password → clicks "Log In" → redirected to /chat + const src = readLoginPage(); + + // Form has onSubmit handler + expect(src).toMatch(/onSubmit=\{handleSubmit\}/); + + // signIn.email called with { email, password } + expect(src).toMatch(/authClient\.signIn\.email\(\s*\{/); + + // Navigates to /chat + expect(src).toMatch(/navigate\(\s*\{\s*to:\s*["']\/chat["']/); + + // Uses state for form fields + expect(src).toMatch(/useState.*""/); + }); + + // Scenarios — Edge case + it('should redirect already authenticated user from /login to /chat', () => { + // Edge case: User is already authenticated → visiting /login redirects to /chat + const src = readLoginPage(); + + // Checks isAuthenticated + expect(src).toMatch(/isAuthenticated/); + + // Redirects to /chat + expect(src).toMatch(/if\s*\(\s*isAuthenticated\s*\)/); + expect(src).toMatch(/navigate\(\s*\{\s*to:\s*["']\/chat["']/); + }); + + // Scenarios — Error cases + it('should show "Invalid email or password" on wrong credentials', () => { + // Error case: Wrong password → inline error "Invalid email or password", form not cleared + const src = readLoginPage(); + + expect(src).toContain('Invalid email or password'); + }); + + it('should prevent submission with empty fields via HTML5 validation', () => { + // Error case: Empty fields → HTML5 validation prevents submission (required attributes) + const src = readLoginPage(); + + // Both email and password inputs have required attribute + const emailIdx = src.indexOf('id="email"'); + const passwordIdx = src.indexOf('id="password"'); + const emailBlock = src.slice(emailIdx - 50, emailIdx + 200); + const passwordBlock = src.slice(passwordIdx - 50, passwordIdx + 200); + + expect(emailBlock).toContain('required'); + expect(passwordBlock).toContain('required'); + }); + + it('should show connection error when backend is unreachable', () => { + // Error case: Backend unreachable → error message "Unable to connect. Please try again." + const src = readLoginPage(); + + expect(src).toContain('Unable to connect. Please try again.'); + + // Uses try/catch for network errors + expect(src).toMatch(/catch\s*\(/); + }); + + // UX Design elements + it('should render "Triage" heading in Space Grotesk font', () => { + // Verify: heading element has font-heading class, text content is "Triage" + const src = readLoginPage(); + + expect(src).toContain('font-heading'); + expect(src).toContain('Triage'); + }); + + it('should render subheading tagline in Inter font', () => { + // Verify: subheading has font-sans class (Inter) + const src = readLoginPage(); + + expect(src).toContain('font-sans'); + expect(src).toContain('SRE Intelligence Platform'); + }); + + it('should render "Log In" CTA button with orange background', () => { + // Verify: submit button text is "Log In", has bg-orange class + const src = readLoginPage(); + + expect(src).toContain('Log In'); + expect(src).toContain('bg-orange'); + + // Button is type="submit" + expect(src).toMatch(/type="submit"/); + }); + + it('should render footer link to /register', () => { + // Verify: link with text containing "Register" points to /register + const src = readLoginPage(); + + expect(src).toContain('Register'); + expect(src).toMatch(/to="\/register"/); + expect(src).toContain("Don't have an account?"); + }); + }); +}); From 0bc2e3a52b0a84c644e182eee4689b19b760a8ca Mon Sep 17 00:00:00 2001 From: luci-efe Date: Thu, 9 Apr 2026 06:41:21 -0600 Subject: [PATCH 04/11] =?UTF-8?q?test(auth):=20auth=20guard=20tests=20?= =?UTF-8?q?=E2=80=94=20SPEC-20260409-002=20(9/9=20tests)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/fe-auth-pages/auth-guard.test.ts | 184 +++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 tests/fe-auth-pages/auth-guard.test.ts diff --git a/tests/fe-auth-pages/auth-guard.test.ts b/tests/fe-auth-pages/auth-guard.test.ts new file mode 100644 index 0000000..7b30937 --- /dev/null +++ b/tests/fe-auth-pages/auth-guard.test.ts @@ -0,0 +1,184 @@ +/** + * SPEC-20260409-002 — Auth Pages (Login/Register) + * REQ-FE05: Auth Guard (Protected Routes) + * + * Tests for root route auth enforcement — redirect logic, loading state, auth bypass for login/register. + * Validates source code structure and correctness via file inspection. + */ +import { describe, it, expect } from 'vitest'; +import { readFileSync, existsSync } from 'fs'; +import { resolve } from 'path'; + +const PROJECT_ROOT = resolve(__dirname, '../../'); +const ROOT_LAYOUT = resolve(PROJECT_ROOT, 'frontend/src/routes/__root.tsx'); + +function readRootLayout(): string { + expect(existsSync(ROOT_LAYOUT), '__root.tsx must exist').toBe(true); + return readFileSync(ROOT_LAYOUT, 'utf-8'); +} + +describe('SPEC-20260409-002: Auth Pages — Auth Guard', () => { + + // ─── REQ-FE05: Auth Guard (Protected Routes) ─────────────────── + + describe('REQ-FE05: Auth Guard (Protected Routes)', () => { + + // Acceptance Criteria + it('should redirect unauthenticated user to /login for any route except /login or /register', () => { + // GIVEN an unauthenticated user + // WHEN they navigate to any route except /login or /register + // THEN they are redirected to /login + const src = readRootLayout(); + + // Checks !isAuthenticated && !isAuthPage + expect(src).toMatch(/!isAuthenticated\s*&&\s*!isAuthPage/); + + // Redirects to /login + expect(src).toMatch(/Navigate\s+to="\/login"/); + }); + + it('should redirect authenticated user from /login or /register to /chat', () => { + // GIVEN an authenticated user + // WHEN they navigate to /login or /register + // THEN they are redirected to /chat + const src = readRootLayout(); + + // Checks isAuthenticated && isAuthPage + expect(src).toMatch(/isAuthenticated\s*&&\s*isAuthPage/); + + // Redirects to /chat + expect(src).toMatch(/Navigate\s+to="\/chat"/); + }); + + it('should show loading spinner while auth state is loading', () => { + // GIVEN the auth state is loading + // WHEN any page renders + // THEN a loading spinner is shown (not a flash of login page) + const src = readRootLayout(); + + // Checks isLoading + expect(src).toMatch(/if\s*\(\s*isLoading\s*\)/); + + // Shows spinner with animate-spin + expect(src).toContain('animate-spin'); + + // Loading check comes before auth redirect (spinner shown before redirect logic) + const loadingPos = src.indexOf('isLoading'); + const redirectPos = src.indexOf('!isAuthenticated && !isAuthPage'); + expect(loadingPos).toBeGreaterThan(-1); + expect(redirectPos).toBeGreaterThan(-1); + expect(loadingPos).toBeLessThan(redirectPos); + }); + + // Scenarios — Happy path + it('should allow authenticated user to navigate freely between /chat, /board, /settings', () => { + // Happy path: Authenticated user navigates freely between /chat, /board, /settings + // When isAuthenticated is true and isAuthPage is false, no redirect occurs — Outlet renders + const src = readRootLayout(); + + // The layout renders Outlet for authenticated non-auth pages (sidebar + Outlet) + expect(src).toContain(''); + + // Sidebar has navigation links to /chat, /board, /settings + expect(src).toMatch(/to="\/chat"/); + expect(src).toMatch(/to="\/board"/); + expect(src).toMatch(/to="\/settings"/); + + // No blanket redirect for authenticated users on normal pages + // The redirect to /login only fires when !isAuthenticated && !isAuthPage + expect(src).toMatch(/!isAuthenticated\s*&&\s*!isAuthPage/); + }); + + // Scenarios — Edge cases + it('should redirect direct URL access to /chat when unauthenticated', () => { + // Edge case: Direct URL access to /chat while unauthenticated → redirect to /login + const src = readRootLayout(); + + // isAuthPage only matches /login and /register, so /chat is not an auth page + expect(src).toMatch(/pathname\s*===\s*"\/login"/); + expect(src).toMatch(/pathname\s*===\s*"\/register"/); + + // For unauthenticated users on non-auth pages, redirect to /login + expect(src).toMatch(/!isAuthenticated\s*&&\s*!isAuthPage/); + expect(src).toMatch(/Navigate\s+to="\/login"/); + }); + + it('should show spinner then content on browser refresh while authenticated', () => { + // Edge case: Browser refresh while authenticated → loading spinner → content (no flash) + const src = readRootLayout(); + + // isLoading guard comes first, before any redirect + const loadingCheck = src.indexOf('isLoading'); + const unauthRedirect = src.indexOf('!isAuthenticated && !isAuthPage'); + const authRedirect = src.indexOf('isAuthenticated && isAuthPage'); + + expect(loadingCheck).toBeGreaterThan(-1); + expect(unauthRedirect).toBeGreaterThan(-1); + expect(authRedirect).toBeGreaterThan(-1); + + // Loading check is evaluated before both redirect checks + expect(loadingCheck).toBeLessThan(unauthRedirect); + expect(loadingCheck).toBeLessThan(authRedirect); + + // Spinner is rendered during loading + expect(src).toContain('animate-spin'); + }); + + // Scenarios — Error case + it('should redirect to /login when session check fails due to network error', () => { + // Error case: Session check fails (network) → redirect to /login as fallback + // When session fails, isAuthenticated is false and isLoading is false → redirect to /login + const src = readRootLayout(); + + // Uses useAuth() which derives isAuthenticated from session data + expect(src).toMatch(/useAuth\(\)/); + + // Destructures isAuthenticated, isLoading + expect(src).toMatch(/isAuthenticated/); + expect(src).toMatch(/isLoading/); + + // When session fails: isAuthenticated=false, isLoading=false → !isAuthenticated && !isAuthPage → redirect + expect(src).toMatch(/!isAuthenticated\s*&&\s*!isAuthPage/); + expect(src).toMatch(/Navigate\s+to="\/login"/); + }); + + // Implementation specifics + it('should render auth pages without sidebar', () => { + // Verify: /login and /register render Outlet directly without sidebar wrapper + const src = readRootLayout(); + + // isAuthPage check returns just Outlet (no sidebar) + expect(src).toMatch(/if\s*\(\s*isAuthPage\s*\)/); + + // The auth page branch returns directly + // Find the isAuthPage-only block (not "!isAuthPage") + const authPageBlock = src.indexOf('if (isAuthPage)'); + expect(authPageBlock).toBeGreaterThan(-1); + + // After the isAuthPage check, Outlet is returned without the sidebar
+
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 05ad35c..9419f16 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -20,6 +20,10 @@ export default defineConfig({ "/chat": { target: "http://localhost:4111", changeOrigin: true, + // Only proxy POST (AI streaming) — GET /chat is the SPA route + bypass(req) { + if (req.method !== "POST") return req.url; + }, }, }, }, diff --git a/runtime/src/auth/index.ts b/runtime/src/auth/index.ts index 7f88fa5..495ee22 100644 --- a/runtime/src/auth/index.ts +++ b/runtime/src/auth/index.ts @@ -13,6 +13,8 @@ const trustedOrigins = (() => { origins.push(process.env.BETTER_AUTH_URL); } else { origins.push(devOrigin); + // Allow SSH port-forwarded origins in dev (e.g. localhost:3002) + origins.push('http://localhost:3002'); } return origins; })(); From 27f0aefdd6f4e6ee205ecfd6e54b29df2514de5b Mon Sep 17 00:00:00 2001 From: ricardosobr Date: Thu, 9 Apr 2026 09:03:18 -0600 Subject: [PATCH 08/11] =?UTF-8?q?docs(auth):=20update=20QA=20report,=20spe?= =?UTF-8?q?c=20COMPLETE,=20handoff=20doc=20=E2=80=94=20FE-03?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - QA report: replaced with E2E results (33/33 Playwright tests) - Spec: stage QA → COMPLETE, sign-out UI no longer out of scope - PROJECT_STATE: SPEC-20260409-002 marked COMPLETE - Handoff doc: full implementation summary, acceptance criteria, auth flow diagram, file inventory, commit history, run instructions --- PROJECT_STATE.md | 2 +- docs/FE-03-auth-pages-handoff.md | 116 ++++++++++++++++++++ specs/active/SPEC-20260409-002-qa-report.md | 105 +++++++++--------- specs/active/SPEC-20260409-002.md | 4 +- 4 files changed, 175 insertions(+), 52 deletions(-) create mode 100644 docs/FE-03-auth-pages-handoff.md diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index b6ae45f..1ec2d3c 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -12,7 +12,7 @@ | SPEC-20260409-001 | better-auth-drizzle-libsql-auth | QA | 2026-04-09 | Better Auth + Drizzle/LibSQL: schema, Better Auth instance, drizzle-kit push, Zod schemas, route mounting in Mastra | | SPEC-20260408-002 | linear-resend-integration-tools | CODE | 2026-04-08 | Linear ticketing tools, Resend email tools, shared schemas, and runtime integration tests | | SPEC-20260408-003 | langfuse-observability-integration | CODE | 2026-04-08 | OpenRouter Broadcast + Langfuse SDK fallback, Cloudflare tunnel, trace verification | -| SPEC-20260409-002 | fe-auth-pages-login-register | QA | 2026-04-09 | Auth pages (login/register), Better Auth client SDK, auth guard, neumorphic UX | +| SPEC-20260409-002 | fe-auth-pages-login-register | COMPLETE | 2026-04-09 | Auth pages (login/register), Better Auth client SDK, auth guard, logout, neumorphic UX — 33 E2E tests passing | ## Integration Test Status diff --git a/docs/FE-03-auth-pages-handoff.md b/docs/FE-03-auth-pages-handoff.md new file mode 100644 index 0000000..e63a98e --- /dev/null +++ b/docs/FE-03-auth-pages-handoff.md @@ -0,0 +1,116 @@ +# FE-03 — Auth Pages (Login/Register) — Implementation Handoff + +**Branch:** `feature/fe-auth-pages` +**Spec:** SPEC-20260409-002 +**Ticket:** FE-03 — Auth Pages (Login/Register) +**Epic:** Frontend | **Tier:** 1 (Must Ship) | **Estimate:** 2h +**Date:** 2026-04-09 + +--- + +## Ticket Acceptance Criteria — Status + +| # | Criterion | Status | Evidence | +|---|-----------|--------|----------| +| 1 | User can register with email/password | ✅ Done | `/register` page → `signUp.email()` → auto-login → redirect | +| 2 | User can login and be redirected to chat | ✅ Done | `/login` page → `signIn.email()` → redirect to `/chat` | +| 3 | Unauthenticated access redirects to login | ✅ Done | Auth guard in `__root.tsx` — all routes protected | + +## Ticket Requirements — Status + +| Requirement | Status | Details | +|------------|--------|---------| +| Email/password form | ✅ Done | Both login and register pages with validated forms | +| Better Auth client SDK integration | ✅ Done | `auth-client.ts` with `createAuthClient`, basePath `/auth` | +| Redirect to chat after login | ✅ Done | `navigate({ to: "/chat" })` on success | +| Protected routes | ✅ Done | Root layout auth guard with `` | +| Clean, minimal design with shadcn/ui | ✅ Done | Neumorphic cards, navy bg, orange CTA, Space Grotesk headings | +| Dependency: INFRA-02 (Better Auth backend) | ✅ Met | Backend auth at `/auth/*` operational with LibSQL persistence | + +## What Was Built + +### Frontend (6 files) + +| File | What It Does | +|------|-------------| +| `frontend/src/lib/auth-client.ts` | Better Auth client SDK singleton. baseURL from `window.location.origin`, basePath `/auth`. Exports `signIn`, `signUp`, `signOut`. | +| `frontend/src/hooks/use-auth.ts` | `useAuth()` hook wrapping Better Auth's `useSession()`. Returns `{ user, isLoading, isAuthenticated }`. Re-exports `signIn`, `signUp`, `signOut`. | +| `frontend/src/routes/login.tsx` | Login page at `/login`. Email+password form, error handling (API errors, network errors, fallback messages), loading state, redirect if authenticated. | +| `frontend/src/routes/register.tsx` | Register page at `/register`. Name+email+password form, duplicate email detection, password error passthrough, loading state, redirect if authenticated. | +| `frontend/src/routes/__root.tsx` | Root layout with auth guard. Loading spinner → unauthenticated redirect → auth page bypass → authenticated layout with sidebar (nav links, user info, theme toggle, logout button). | +| `frontend/vite.config.ts` | Dev proxy: `/auth` → backend:4111, `/chat` POST-only → backend:4111 (GET serves SPA). | + +### Backend (1 file modified) + +| File | Change | +|------|--------| +| `runtime/src/auth/index.ts` | Added `localhost:3002` to dev trusted origins for SSH port-forwarded testing. | + +### Infrastructure (1 file modified) + +| File | Change | +|------|--------| +| `docker-compose.override.yml` | Fixed `mastra dev --port 4111` → `PORT=4111` env var (mastra CLI dropped `--port` flag). | + +### Tests (E2E with Playwright) + +| File | Tests | What It Covers | +|------|-------|----------------| +| `tests/fe-auth-pages/auth-e2e.e2e.ts` | 33 | Full E2E against running stack: health checks, auth guard redirects, login/register rendering, real sign-up/sign-in flows, error handling, sidebar verification, authenticated redirects, proxy integration, page navigation | +| `playwright.config.ts` | — | Playwright config: chromium headless, baseURL localhost:3001 | + +**All 33 E2E tests pass against the real stack (Vite + Mastra + Better Auth + LibSQL). Zero mocks.** + +## Auth Flow + +``` +Unauthenticated user → any route → auth guard → redirect /login + ↓ + /login ← fill form → signIn.email() + ↓ + success → redirect /chat → sidebar with user info + error → show alert (invalid creds / network error) + +New user → /register → fill form → signUp.email() + ↓ + success → auto-login → redirect /chat + error → "already exists" / password error / network error + +Authenticated user → /login or /register → redirect to /chat +Authenticated user → click logout → signOut() → redirect /login +``` + +## How to Run + +```bash +# Start the stack +cd runtime && PORT=4111 npx mastra dev & +cd frontend && npx vite --port 3001 --host & + +# Run E2E tests +npm run test:e2e:auth + +# Or run all E2E tests +npm run test:e2e +``` + +Requires LibSQL running on `:8080` (via docker-compose or direct). + +## Commits on This Branch + +| Hash | Message | +|------|---------| +| `5218fc1` | feat(auth): auth client SDK, useAuth hook, vite proxy | +| `2ba6bee` | feat(auth): register page + auth guard activation | +| `f9facf0` | feat(auth): login page with neumorphic UX | +| `0bc2e3a` | test(auth): auth guard tests | +| `8d58853` | qa(auth): QA report GO — 53/53 tests, 6/6 REQs | +| `eda6eca` | test(auth): rewrite tests as E2E with Playwright — 33/33 | +| `621dc7c` | fix(auth): logout button, /chat proxy bypass, dev trusted origin | + +## Known Limitations + +- No password confirmation field on register (acceptable for hackathon) +- No forgot password flow (out of scope per spec) +- No OAuth/social login (out of scope per spec) +- Spec says `max-w-sm` for cards, implementation uses `max-w-md` (cosmetic) diff --git a/specs/active/SPEC-20260409-002-qa-report.md b/specs/active/SPEC-20260409-002-qa-report.md index f109234..d08cc7e 100644 --- a/specs/active/SPEC-20260409-002-qa-report.md +++ b/specs/active/SPEC-20260409-002-qa-report.md @@ -1,38 +1,51 @@ # QA Report — SPEC-20260409-002: Auth Pages (Login/Register) **Date:** 2026-04-09 -**Inspector:** Lyra (QA Warden) +**Inspector:** Lyra (QA Warden) — updated post E2E rewrite **Recommendation:** GO ✅ ## Test Results -- **Total:** 53 | **Passed:** 53 | **Failed:** 0 | **Skipped:** 0 -- **Test files:** 5 passed (5) -- **Duration:** 475ms -- **No regressions** against existing project test suite (439 other tests still passing) +### E2E Tests (Playwright — real stack, zero mocks) -| Test File | Tests | Status | +- **Total:** 33 | **Passed:** 33 | **Failed:** 0 | **Skipped:** 0 +- **Test file:** auth-e2e.e2e.ts +- **Duration:** ~16s +- **Stack:** Frontend (Vite :3001) + Backend (Mastra :4111) + LibSQL + Better Auth +- **Browser:** Chromium headless + +| Test Group | Tests | Status | |-----------|-------|--------| -| auth-client.test.ts | 14 | ✅ PASS | -| auth-guard.test.ts | 9 | ✅ PASS | -| login-page.test.ts | 13 | ✅ PASS | -| register-page.test.ts | 12 | ✅ PASS | -| vite-proxy.test.ts | 5 | ✅ PASS | +| Preconditions (health checks) | 3 | ✅ PASS | +| Auth Guard — unauthenticated redirect | 3 | ✅ PASS | +| Login Page Rendering | 7 | ✅ PASS | +| Register Page Rendering | 5 | ✅ PASS | +| Register Flow (real sign-up) | 2 | ✅ PASS | +| Login Flow (real sign-in) | 4 | ✅ PASS | +| Authenticated User Experience (sidebar) | 7 | ✅ PASS | +| Auth Proxy Integration | 1 | ✅ PASS | +| Navigation (cross-links) | 2 | ✅ PASS | + +### Previous Tests (replaced) + +The original 53 unit tests (5 files) used `readFileSync` + `toContain` — static +string-matching with zero behavioral coverage. They were deleted and replaced +with the E2E suite above, which tests real user flows against the running stack. ## Requirements Validation -| Req ID | Description | Priority | Verdict | Test File | -|--------|-------------|----------|---------|-----------| -| REQ-FE01 | Better Auth Client SDK Setup | P0 | ✅ PASS | auth-client.test.ts (6 tests) | -| REQ-FE02 | useAuth Hook Implementation | P0 | ✅ PASS | auth-client.test.ts (8 tests) | -| REQ-FE03 | Login Page | P0 | ✅ PASS | login-page.test.ts (13 tests) | -| REQ-FE04 | Register Page | P0 | ✅ PASS | register-page.test.ts (12 tests) | -| REQ-FE05 | Auth Guard (Protected Routes) | P0 | ✅ PASS | auth-guard.test.ts (9 tests) | -| REQ-FE06 | Vite Dev Proxy Configuration | P1 | ✅ PASS | vite-proxy.test.ts (5 tests) | +| Req ID | Description | Priority | Verdict | Evidence | +|--------|-------------|----------|---------|----------| +| REQ-FE01 | Better Auth Client SDK Setup | P0 | ✅ PASS | Proxy test confirms frontend→backend auth flow works | +| REQ-FE02 | useAuth Hook Implementation | P0 | ✅ PASS | Sidebar shows real user name/email from session | +| REQ-FE03 | Login Page | P0 | ✅ PASS | Full login flow: render→fill→submit→redirect→sidebar | +| REQ-FE04 | Register Page | P0 | ✅ PASS | Full register flow: render→fill→submit→auto-login→sidebar | +| REQ-FE05 | Auth Guard (Protected Routes) | P0 | ✅ PASS | /, /board, /settings all redirect to /login; /login, /register redirect to app when authenticated | +| REQ-FE06 | Vite Dev Proxy Configuration | P1 | ✅ PASS | API request through :3001/auth/* returns real user data | **Requirements satisfied: 6/6 (100%)** -## Implementation Files Verification +## Implementation Files | File | Lines | Status | Purpose | |------|-------|--------|---------| @@ -40,42 +53,36 @@ | frontend/src/hooks/use-auth.ts | 37 | ✅ Rewritten | useAuth hook with real Better Auth session | | frontend/src/routes/login.tsx | 123 | ✅ Created | Login page with neumorphic UX | | frontend/src/routes/register.tsx | 149 | ✅ Created | Register page with neumorphic UX | -| frontend/src/routes/__root.tsx | 135 | ✅ Modified | Auth guard activated, dynamic user in sidebar | -| frontend/vite.config.ts | 26 | ✅ Modified | Dev proxy for /auth and /chat | - -## UX Design Spec Compliance - -| Criterion | Verified | -|-----------|----------| -| Neumorphic card (shadow-neu-raised) | ✅ Login + Register | -| Inset inputs (shadow-neu-inset) | ✅ Login + Register | -| Navy background (bg-navy) | ✅ Login + Register | -| Orange CTA (bg-orange) | ✅ Login + Register | -| Space Grotesk heading (font-heading) | ✅ Login + Register | -| Inter body text (font-sans) | ✅ Login + Register | -| "Triage" product name heading | ✅ Login + Register | -| Post-login redirect to /chat | ✅ Both pages | -| Cross-links between login/register | ✅ Both pages | +| frontend/src/routes/__root.tsx | 145 | ✅ Modified | Auth guard, sidebar user info, logout button | +| frontend/vite.config.ts | 30 | ✅ Modified | Dev proxy /auth + /chat (POST only) | +| runtime/src/auth/index.ts | 51 | ✅ Modified | Added dev trusted origin for SSH port forwarding | +| playwright.config.ts | 20 | ✅ Created | E2E test configuration | +| tests/fe-auth-pages/auth-e2e.e2e.ts | 355 | ✅ Created | 33 E2E behavioral tests | ## Linear Issue Acceptance Criteria -| Criterion | Verdict | -|-----------|---------| -| User can register with email/password | ✅ PASS — signUp.email() called with {name, email, password} | -| User can login and be redirected to chat | ✅ PASS — signIn.email() + navigate to /chat | -| Unauthenticated access redirects to login | ✅ PASS — auth guard in __root.tsx | +| Criterion | Verdict | Evidence | +|-----------|---------|----------| +| User can register with email/password | ✅ PASS | E2E test #19: real sign-up → auto-login → sidebar shows user | +| User can login and be redirected to chat | ✅ PASS | E2E test #21: real sign-in → redirect → sidebar visible | +| Unauthenticated access redirects to login | ✅ PASS | E2E tests #4-6: /, /board, /settings → /login | -## Issues Found +## Additional Fixes (post-QA) -None. +| Fix | Description | +|-----|-------------| +| Logout button | Added sign-out button (LogOut icon) in sidebar user section | +| /chat proxy bypass | GET /chat serves SPA, only POST /chat proxies to backend | +| docker-compose.override | `mastra dev --port` → `PORT=4111` env var (CLI flag removed) | +| Dev trusted origin | `localhost:3002` for SSH port-forwarded testing | -## Security Notes +## Issues Found -- Auth cookies are HttpOnly (set by backend) — frontend cannot read tokens directly -- Session check via GET /auth/get-session (not client-side token parsing) -- Vite proxy only active in dev mode (production uses Caddy reverse proxy) -- No secrets in frontend code +- **Spec says max-w-sm, implementation uses max-w-md** — cosmetic, not blocking +- **Spec listed "Sign-out UI" as out of scope** — implemented anyway (logout button added) ## Recommendation -**GO** ✅ — All 6 requirements satisfied with 53 passing tests. Zero skipped, zero failures, zero regressions. UX design spec fully implemented. Linear acceptance criteria met. Ready for COMPLETE stage. +**GO** ✅ — All 6 requirements verified with 33 E2E tests against the real stack. +Zero mocks. Login, register, auth guard, logout, and proxy all functional end-to-end. +Backend (Better Auth + Drizzle + LibSQL) confirmed operational. Ready for COMPLETE stage. diff --git a/specs/active/SPEC-20260409-002.md b/specs/active/SPEC-20260409-002.md index 6f13939..ac020b3 100644 --- a/specs/active/SPEC-20260409-002.md +++ b/specs/active/SPEC-20260409-002.md @@ -2,7 +2,7 @@ **ID:** SPEC-20260409-002 **Name:** fe-auth-pages-login-register -**Stage:** QA +**Stage:** COMPLETE **Created:** 2026-04-09 **Updated:** 2026-04-09 **Author:** Chenko @@ -33,7 +33,7 @@ Implement login and registration pages for the Triage app, connecting the fronte - Email verification flow - User profile/settings page - Session refresh/token rotation (backend handles 7-day fixed expiry) -- Sign-out UI (future ticket) +- ~~Sign-out UI (future ticket)~~ — implemented: logout button in sidebar ## Dependencies From c20aefbaca488980f81c66daca3644086f5cab5e Mon Sep 17 00:00:00 2001 From: ricardosobr Date: Thu, 9 Apr 2026 09:07:44 -0600 Subject: [PATCH 09/11] chore: update runtime lockfile, ignore test-results --- .gitignore | 1 + runtime/package-lock.json | 66 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index fc1bf9f..249eb70 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ HACKATHON_BRAINSTORM_OUTPUT/ docs/drizzle-libsql-reference.md config/ *.bak +test-results/ diff --git a/runtime/package-lock.json b/runtime/package-lock.json index bedd1c0..040c74b 100644 --- a/runtime/package-lock.json +++ b/runtime/package-lock.json @@ -17,13 +17,13 @@ "@openrouter/ai-sdk-provider": "^2.5.0", "@opentelemetry/api": "^1.9.1", "better-auth": "^1.0.0", - "drizzle-kit": "^0.30.0", "drizzle-orm": "^0.38.0", "resend": "^6.10.0", "zod": "^3.25.20" }, "devDependencies": { "@types/node": "^22.15.0", + "drizzle-kit": "^0.30.0", "mastra": "^1.4.1", "typescript": "~5.8.3", "vitest": "^3.1.1" @@ -829,6 +829,7 @@ "version": "0.10.2", "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", "integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==", + "dev": true, "license": "Apache-2.0" }, "node_modules/@esbuild-kit/core-utils": { @@ -836,6 +837,7 @@ "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", "integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==", "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, "license": "MIT", "dependencies": { "esbuild": "~0.18.20", @@ -849,6 +851,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -865,6 +868,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -881,6 +885,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -897,6 +902,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -913,6 +919,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -929,6 +936,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -945,6 +953,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -961,6 +970,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -977,6 +987,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -993,6 +1004,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1009,6 +1021,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1025,6 +1038,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1041,6 +1055,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1057,6 +1072,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1073,6 +1089,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1089,6 +1106,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1105,6 +1123,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1121,6 +1140,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1137,6 +1157,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1153,6 +1174,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1169,6 +1191,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1185,6 +1208,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1198,6 +1222,7 @@ "version": "0.18.20", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -1236,6 +1261,7 @@ "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", "integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==", "deprecated": "Merged into tsx: https://tsx.is", + "dev": true, "license": "MIT", "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", @@ -2716,6 +2742,7 @@ "version": "3.9.3", "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.3.tgz", "integrity": "sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==", + "dev": true, "license": "MIT" }, "node_modules/@pinojs/redact": { @@ -4358,6 +4385,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, "license": "MIT" }, "node_modules/bytes": { @@ -5093,6 +5121,7 @@ "version": "0.30.6", "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.30.6.tgz", "integrity": "sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g==", + "dev": true, "license": "MIT", "dependencies": { "@drizzle-team/brocli": "^0.10.2", @@ -5112,6 +5141,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5128,6 +5158,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5144,6 +5175,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5160,6 +5192,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5176,6 +5209,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5192,6 +5226,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5208,6 +5243,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5224,6 +5260,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5240,6 +5277,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5256,6 +5294,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5272,6 +5311,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5288,6 +5328,7 @@ "cpu": [ "loong64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5304,6 +5345,7 @@ "cpu": [ "mips64el" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5320,6 +5362,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5336,6 +5379,7 @@ "cpu": [ "riscv64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5352,6 +5396,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5368,6 +5413,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5384,6 +5430,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5400,6 +5447,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5416,6 +5464,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5432,6 +5481,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5448,6 +5498,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5464,6 +5515,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5477,6 +5529,7 @@ "version": "0.19.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -5710,6 +5763,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" @@ -5801,6 +5855,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "dev": true, "license": "MIT", "dependencies": { "debug": "^4.3.4" @@ -6452,6 +6507,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/gel/-/gel-2.2.0.tgz", "integrity": "sha512-q0ma7z2swmoamHQusey8ayo8+ilVdzDt4WTxSPzq/yRqvucWRfymRVMvNgmSC0XK7eNjjEZEcplxpgaNojKdmQ==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@petamoriken/float16": "^3.8.7", @@ -6472,6 +6528,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -6481,6 +6538,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^3.1.1" @@ -6572,6 +6630,7 @@ "version": "4.13.7", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -9233,6 +9292,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" @@ -9439,6 +9499,7 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -9664,6 +9725,7 @@ "version": "1.8.3", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -9784,6 +9846,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -9803,6 +9866,7 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", From bb6f8569d90e0cf3b18da83b8c450b8b754ad2ed Mon Sep 17 00:00:00 2001 From: ricardosobr Date: Thu, 9 Apr 2026 09:41:24 -0600 Subject: [PATCH 10/11] fix(auth): restore localhost:3002 trusted origin for SSH port-forward dev testing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Temporary: allows local machine to test via SSH tunnel (port 3002). Intentionally kept on this branch only — dev branch does not have this. Co-Authored-By: Claude Sonnet 4.6 --- runtime/src/auth/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/runtime/src/auth/index.ts b/runtime/src/auth/index.ts index 7f88fa5..495ee22 100644 --- a/runtime/src/auth/index.ts +++ b/runtime/src/auth/index.ts @@ -13,6 +13,8 @@ const trustedOrigins = (() => { origins.push(process.env.BETTER_AUTH_URL); } else { origins.push(devOrigin); + // Allow SSH port-forwarded origins in dev (e.g. localhost:3002) + origins.push('http://localhost:3002'); } return origins; })(); From 7dfb9d94e2e709efbc4dd6725902398af4b184a9 Mon Sep 17 00:00:00 2001 From: ricardosobr Date: Thu, 9 Apr 2026 11:46:21 -0600 Subject: [PATCH 11/11] =?UTF-8?q?fix(infra):=20Docker=20environment=20fixe?= =?UTF-8?q?s=20=E2=80=94=20init-db,=20LibSQL=20URL,=20CSP,=20trusted=20ori?= =?UTF-8?q?gins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Post-merge fixes to make `docker compose up --build` work out of the box: - Add runtime/init-db.mjs for idempotent table creation on startup - Fix mastra dev overriding LIBSQL_URL from baked-in .env (sed workaround) - Relax Caddyfile CSP in dev-frontend for Vite HMR inline scripts - Add localhost:3002 to Better Auth trustedOrigins for SSH tunnel testing - Set NODE_ENV=development in override, Vite npm install + named volume - Remove duplicate registerApiRoute import (merge artifact) Co-Authored-By: Claude Opus 4.6 --- Caddyfile | 1 + Dockerfile.runtime | 3 +- docker-compose.override.yml | 14 ++- docs/FE-03-docker-infra-fixes-handoff.md | 110 +++++++++++++++++++++++ runtime/init-db.mjs | 90 +++++++++++++++++++ runtime/src/lib/auth.ts | 2 + runtime/src/mastra/index.ts | 1 - 7 files changed, 215 insertions(+), 6 deletions(-) create mode 100644 docs/FE-03-docker-infra-fixes-handoff.md create mode 100644 runtime/init-db.mjs diff --git a/Caddyfile b/Caddyfile index 9ab5ca3..1de69b1 100644 --- a/Caddyfile +++ b/Caddyfile @@ -3,6 +3,7 @@ # In dev mode, config.json is proxied to Vite along with all other frontend requests (dev-frontend) { + header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' ws://vite:5173 ws://localhost:5173; font-src 'self'; frame-ancestors 'none'" reverse_proxy vite:5173 } diff --git a/Dockerfile.runtime b/Dockerfile.runtime index 4ad044b..bddbf58 100644 --- a/Dockerfile.runtime +++ b/Dockerfile.runtime @@ -15,7 +15,8 @@ RUN if [ -f runtime/src/mastra/index.ts ]; then \ FROM node:22-alpine WORKDIR /app COPY --from=builder /app/output/ ./ +COPY runtime/init-db.mjs ./init-db.mjs EXPOSE 4111 ENV PORT=4111 NODE_ENV=production USER node -CMD ["node", "index.mjs"] +CMD ["sh", "-c", "node init-db.mjs && node index.mjs"] diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 0c2bedc..67401cc 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -7,18 +7,18 @@ services: vite: image: node:22-alpine working_dir: /app - command: npx vite --host 0.0.0.0 + command: sh -c "npm install && npx vite --host 0.0.0.0" ports: - "127.0.0.1:5173:5173" volumes: - ./frontend:/app - - /app/node_modules + - vite_node_modules:/app/node_modules networks: - app frontend: environment: - FRONTEND_MODE: dev + FRONTEND_MODE: static runtime: build: @@ -26,8 +26,14 @@ services: dockerfile: Dockerfile.runtime target: builder working_dir: /app/runtime - command: npx mastra dev + command: sh -c "sed -i 's|LIBSQL_URL=.*|LIBSQL_URL=http://libsql:8080|' /app/runtime/.env && node /app/runtime/init-db.mjs && npx mastra dev" environment: - PORT=4111 + - LIBSQL_URL=http://libsql:8080 + - NODE_ENV=development volumes: - ./runtime/src:/app/runtime/src + - ./runtime/init-db.mjs:/app/runtime/init-db.mjs + +volumes: + vite_node_modules: diff --git a/docs/FE-03-docker-infra-fixes-handoff.md b/docs/FE-03-docker-infra-fixes-handoff.md new file mode 100644 index 0000000..b11149e --- /dev/null +++ b/docs/FE-03-docker-infra-fixes-handoff.md @@ -0,0 +1,110 @@ +# FE-03 — Docker Infrastructure Fixes — Handoff + +**Branch:** `feature/fe-auth-pages` +**Date:** 2026-04-09 +**Context:** After merging dev (`5ac0512`), the Docker environment was broken — runtime crashed on startup, frontend served stale builds, auth tables didn't exist. These fixes make `docker compose up --build` work out of the box. + +--- + +## Summary of Issues & Fixes + +### 1. Runtime crash: LibSQL ECONNREFUSED (localhost:8080) + +**Root cause:** `mastra dev` uses dotenv to load `runtime/.env`, which contains `LIBSQL_URL=http://localhost:8080` (for local dev without Docker). This overrode the Docker-injected env var `http://libsql:8080`. + +**Fix (docker-compose.override.yml):** `sed` rewrites `LIBSQL_URL` in the baked-in `.env` before `mastra dev` starts. Also sets `LIBSQL_URL=http://libsql:8080` explicitly in the environment block. + +### 2. Database tables missing: "no such table: auth_user" + +**Root cause:** Fresh LibSQL container has no tables. `drizzle-kit push` requires interactive TTY (unsuitable for Docker). + +**Fix:** Created `runtime/init-db.mjs` — runs `CREATE TABLE IF NOT EXISTS` for all 7 tables (auth_user, auth_session, auth_account, auth_verification, wiki_documents, wiki_chunks, local_tickets). Executes before the server starts in both dev and production Docker. + +### 3. Vite container crash: ERR_MODULE_NOT_FOUND + +**Root cause:** Anonymous volume `/app/node_modules` was empty with no `npm install` step. + +**Fix (docker-compose.override.yml):** Changed Vite command to `sh -c "npm install && npx vite --host 0.0.0.0"` and switched to a named volume `vite_node_modules`. + +### 4. CSP blocks Vite inline scripts in dev mode + +**Root cause:** Caddy's `Content-Security-Policy` header had `script-src 'self'`, blocking Vite's HMR preamble injection (`@vitejs/plugin-react can't detect preamble`). + +**Fix (Caddyfile):** Dev-frontend snippet overrides CSP to allow `'unsafe-inline'` and `'unsafe-eval'` for scripts, plus `ws://` for Vite HMR WebSocket. + +### 5. Duplicate import in mastra/index.ts + +**Root cause:** Merge conflict left duplicate `import { registerApiRoute }` on lines 2 and 5, causing `mastra build` to fail. + +**Fix:** Removed the duplicate import line. + +### 6. Auth 403 "invalid origin" via SSH tunnel + +**Root cause:** Better Auth `trustedOrigins` only included `localhost:3001`. SSH port-forward testing uses `localhost:3002`. Also, `NODE_ENV=production` was set by Dockerfile even in dev override (via `target: builder` inheriting base image state). + +**Fix:** Added `http://localhost:3002` to dev trustedOrigins in `runtime/src/lib/auth.ts`. Added `NODE_ENV=development` to docker-compose.override.yml environment. + +--- + +## Files Changed + +| File | Change | +|------|--------| +| `runtime/init-db.mjs` | **New.** Creates all 7 DB tables idempotently on startup. | +| `Dockerfile.runtime` | Copies `init-db.mjs` into production image. CMD runs `node init-db.mjs && node index.mjs`. | +| `docker-compose.override.yml` | Vite: `npm install` + named volume. Runtime: `sed` fix for .env, `LIBSQL_URL` + `NODE_ENV=development` env vars, init-db mount. Frontend: `FRONTEND_MODE: static`. | +| `Caddyfile` | Dev-frontend snippet: relaxed CSP for Vite HMR (unsafe-inline, unsafe-eval, ws://). | +| `runtime/src/lib/auth.ts` | Added `localhost:3002` to dev trustedOrigins for SSH tunnel testing. | +| `runtime/src/mastra/index.ts` | Removed duplicate `registerApiRoute` import (merge artifact). | + +## Architecture Notes + +### Dev mode (docker-compose.override.yml auto-loaded) + +``` +Browser → Caddy:3001 (static files from /srv/) → runtime:4111 (/api/*, /auth/*) + ↕ + LibSQL:8080 + +Runtime runs: sed .env → init-db.mjs → mastra dev (hot reload) +``` + +### Production mode (`docker compose -f docker-compose.yml up --build`) + +``` +Browser → Caddy:3001 (static files from /srv/) → runtime:4111 (/api/*, /auth/*) + ↕ + LibSQL:8080 + +Runtime runs: node init-db.mjs && node index.mjs +``` + +### SSH tunnel testing + +``` +Local browser:3002 → SSH tunnel → Server:3001 → Caddy → runtime:4111 +``` + +Requires `http://localhost:3002` in Better Auth trustedOrigins (dev only). + +## How to Run + +```bash +# Dev mode (default — auto-loads override) +docker compose up --build + +# Production mode (no override, no Vite, no hot reload) +docker compose -f docker-compose.yml up --build + +# SSH tunnel from local machine +ssh -L 3002:localhost:3001 agent@ +# Then open http://localhost:3002 +``` + +## Verified Auth Flow (E2E in Docker) + +1. Navigate to `/` → auth guard redirects to `/login` +2. Click "Register" → fill form → Create Account → redirects to `/chat` with user info in sidebar +3. Click "Sign out" → redirects to `/login` +4. Fill login form → Log In → redirects to `/chat` +5. All tested via Playwright (server-side) and manual SSH tunnel (client-side) diff --git a/runtime/init-db.mjs b/runtime/init-db.mjs new file mode 100644 index 0000000..49e7298 --- /dev/null +++ b/runtime/init-db.mjs @@ -0,0 +1,90 @@ +/** + * Create auth and app tables in LibSQL if they don't exist. + * Runs before the server starts in both dev and production Docker. + */ +import { createClient } from '@libsql/client'; + +const url = process.env.LIBSQL_URL || 'http://libsql:8080'; +const client = createClient({ url }); + +const tables = [ + `CREATE TABLE IF NOT EXISTS auth_user ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + email TEXT NOT NULL UNIQUE, + email_verified INTEGER NOT NULL DEFAULT 0, + image TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS auth_session ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES auth_user(id) ON DELETE CASCADE, + expires_at INTEGER NOT NULL, + token TEXT NOT NULL UNIQUE, + ip_address TEXT, + user_agent TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS auth_account ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL REFERENCES auth_user(id) ON DELETE CASCADE, + account_id TEXT NOT NULL, + provider_id TEXT NOT NULL, + access_token TEXT, + refresh_token TEXT, + id_token TEXT, + access_token_expires_at INTEGER, + refresh_token_expires_at INTEGER, + scope TEXT, + password TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS auth_verification ( + id TEXT PRIMARY KEY, + identifier TEXT NOT NULL, + value TEXT NOT NULL, + expires_at INTEGER NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS wiki_documents ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL, + file_path TEXT NOT NULL, + summary TEXT NOT NULL, + pass INTEGER NOT NULL, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS wiki_chunks ( + id TEXT PRIMARY KEY, + document_id TEXT NOT NULL REFERENCES wiki_documents(id) ON DELETE CASCADE, + content TEXT NOT NULL, + chunk_index INTEGER NOT NULL, + embedding F32_BLOB(1536), + created_at INTEGER NOT NULL + )`, + `CREATE TABLE IF NOT EXISTS local_tickets ( + id TEXT PRIMARY KEY, + linear_issue_id TEXT, + title TEXT NOT NULL, + description TEXT NOT NULL, + severity TEXT NOT NULL, + priority INTEGER NOT NULL, + status TEXT NOT NULL DEFAULT 'triage', + assignee_id TEXT REFERENCES auth_user(id), + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + synced_at INTEGER + )`, +]; + +for (const sql of tables) { + const name = sql.match(/CREATE TABLE IF NOT EXISTS (\w+)/)[1]; + await client.execute(sql); + console.log(`[init-db] ${name} OK`); +} +console.log('[init-db] All tables ready'); diff --git a/runtime/src/lib/auth.ts b/runtime/src/lib/auth.ts index e47baf9..122937f 100644 --- a/runtime/src/lib/auth.ts +++ b/runtime/src/lib/auth.ts @@ -13,6 +13,8 @@ const trustedOrigins = (() => { origins.push(process.env.BETTER_AUTH_URL); } else { origins.push(devOrigin); + // Allow SSH port-forward dev testing (localhost:3002) + origins.push('http://localhost:3002'); } return origins; })(); diff --git a/runtime/src/mastra/index.ts b/runtime/src/mastra/index.ts index 2d0e104..87fd743 100644 --- a/runtime/src/mastra/index.ts +++ b/runtime/src/mastra/index.ts @@ -2,7 +2,6 @@ import { Mastra } from '@mastra/core'; import { registerApiRoute } from '@mastra/core/server'; import { LibSQLStore } from '@mastra/libsql'; import { chatRoute } from '@mastra/ai-sdk'; -import { registerApiRoute } from '@mastra/core/server'; import { LinearClient } from '@linear/sdk'; import type { Context } from 'hono';