# padhle — Codebase Analysis & Feature Tracker
## File Map
| File | Purpose |
|---|---|
| `frontend/src/App.jsx` | Main orchestrator — owns all state, SSE chat logic, session CRUD, auth gating |
| `frontend/src/styles/global.css` | Global CSS variables (colors, fonts, spacing, radius, shadows) |
| `frontend/src/main.jsx` | React entry point — wraps App in `AuthProvider` |
| `frontend/src/lib/auth/SupabaseAuth.jsx` | Auth context — `signIn`, `signUp`, `signOut`, `getToken` (all via backend) |
| `frontend/src/lib/auth/backendAuth.js` | Backend auth client — `signup`, `signin`, `signout`, `me`, `check` |
| `frontend/src/lib/auth/SignIn.jsx` | Sign-in/Sign-up modal form |
| `frontend/src/lib/auth/SignIn.css` | Modal styles |
| `frontend/src/components/selector-flow/SelectorFlow.jsx` | Generic step-based selector — grade, subject, chapter (grid layout, conditional icons) |
| `frontend/src/components/selector-flow/SelectorFlow.css` | Grid layout for grade options |
| `frontend/src/components/top-nav/TopNav.jsx` | Header with grade/subject/chapter dropdown pills + search/notifications/user avatar |
| `frontend/src/components/top-nav/TopNav.css` | Pill buttons, dropdown menus, avatar, sign-out styles |
| `frontend/src/components/sidebar/Sidebar.jsx` | Brand, New Chat button, subjects list, chat history list, user info, sign-out, upgrade card |
| `frontend/src/components/sidebar/Sidebar.css` | Sidebar layout, hover indicators, chat item delete, upgrade card, user info |
| `frontend/src/components/chat-history/ChatHistory.jsx` | Welcome screen with bento suggestions OR message list OR selector flow |
| `frontend/src/components/chat-history/ChatHistory.css` | Welcome header, bento grid, info panel, message list |
| `frontend/src/components/message/Message.jsx` | Single message bubble (user or assistant), action buttons, typing dots |
| `frontend/src/components/message/Message.css` | Bubble styling, avatar, actions, typing animation |
| `frontend/src/components/chat-input/ChatInput.jsx` | Textarea with auto-expand, mic, send button, attachment |
| `frontend/src/components/chat-input/ChatInput.css` | Input bar styling, send button states, disclaimer |
| `backend/src/index.js` | Express app setup, middleware, route registration |
| `backend/src/middleware/cookies.js` | Cookie parser middleware (cookie-parser) |
| `backend/src/middleware/supabaseAuth.js` | JWT verification middleware — reads from `padhle.token` httpOnly cookie |
| `backend/src/routes/auth.js` | POST /api/auth/signup, /api/auth/signin, /api/auth/signout; GET /api/auth/me |
| `backend/src/routes/chat.js` | POST /api/chat (stream), GET /api/chat/sessions, GET /api/chat/:id (load session+messages); validates metadata and enforces ownership |
| `backend/src/routes/sessions.js` | GET/POST/DELETE/PATCH /api/sessions; explicit ownership checks |
| `backend/src/services/db.js` | Supabase database service for profiles, sessions, and messages |
| `backend/src/routes/chatValidation.js` | Pure chat input validation and session ownership helpers |
| `backend/src/stores/sessionStore.js` | Legacy in-memory Map-based session + message store; no longer used by chat/session routes |
| `frontend/src/appState.js` | Central default/reset state for auth transitions |
| `backend/src/services/ai.js` | AI provider abstraction (OpenAI/Anthropic/Google), streaming, system prompt builder |
---
## Architecture
```
Frontend (React + Vite) ──SSE──▶ Backend (Express) ──API──▶ AI Provider
│ │
├─ State: activeGrade, └─ Supabase Postgres service
activeSubject, └─ Explicit req.user.uid ownership checks
activeChapter,
messages,
selectorStep (grade → subject → chapter → chat)
│
└─ Auth: httpOnly cookies (padhle.token, padhle.user)
└─ Browser API requests use credentials: 'include'
```
---
## Authentication — How It Works
All authentication is handled server-side via httpOnly cookies. The frontend never sees raw tokens.
### Flow
```
1. User enters email + password → Frontend POST /api/auth/signin
2. Backend calls Supabase Auth API → receives JWT
3. Backend sets httpOnly cookies:
- padhle.token = JWT (for API verification)
- padhle.user = JSON({ uid, email }) (safe user metadata)
4. Frontend never sees the JWT — browser stores cookies automatically
5. All API calls include cookies via credentials: 'include'
6. Backend middleware reads padhle.token, verifies via /auth/v1/user
7. If valid, req.user = { uid, email } is attached to request
```
### Auth Endpoints
| Route | Method | Purpose | Auth Required |
|---|---|---|---|
| `/api/auth/signup` | POST | Create account via Supabase Auth | No |
| `/api/auth/signin` | POST | Sign in via Supabase Auth | No |
| `/api/auth/signout` | POST | Revoke session + clear cookies | Yes |
| `/api/auth/me` | GET | Get current user from cookie | Yes |
### Protected Routes
| Route | Middleware | Purpose |
|---|---|---|
| `/api/chat` | supabaseAuth | Send message, load conversation |
| `/api/sessions` | supabaseAuth | CRUD chat sessions |
### Security
- **httpOnly cookies**: JWT is never accessible to JavaScript
- **sameSite: lax**: CSRF protection
- **secure flag**: Enabled in production (HTTPS)
- **JWT verification**: Each request calls Supabase Auth server to validate
- **No localStorage tokens**: Sensitive data never stored in browser
- **Session revocation**: Signout calls Supabase `/auth/v1/logout` server-side
---
## Grade Selector — Current State
### Where it lives
| Component | File | Role |
|---|---|---|
| `SelectorFlow` | `frontend/src/components/selector-flow/SelectorFlow.jsx` | Generic step-based selector — grade, subject, chapter (grid layout) |
| `TopNav` grade pill | `frontend/src/components/top-nav/TopNav.jsx` (lines ~46-75) | Always-visible header dropdown |
| State owner | `frontend/src/App.jsx` | `selectorStep` + `activeGrade/Subject/Chapter` state |
| `ChatHistory` | `frontend/src/components/chat-history/ChatHistory.jsx` | Renders SelectorFlow when `selectorStep !== "chat"` |
### Sequential selector flow
```
New Chat → selectorStep = "grade"
→ Pick Grade → selectorStep = "subject" → fade-in
→ Pick Subject → selectorStep = "chapter" → fade-in (subject-aware chapters)
→ Pick Chapter → selectorStep = "chat" → welcome screen / messages
```
### State values
| State | Type | Default | Stored in DB |
|---|---|---|---|
| `activeGrade` | label | `"Choose standard"` | `"Grade 10"` |
| `activeSubject` | ID | `"choose-subject"` | `"math"` |
| `activeChapter` | label | `"choose-chapter"` | `"Chapter 1: Rational Numbers"` |
| `selectorStep` | enum | `"grade"` | — |
| Value | Used in |
|---|---|
| `"Choose standard"` | Default for `activeGrade` |
| `Grade 6` .. `Grade 12` | SelectorFlow + TopNav grade options |
| `"math"` .. `"geography"` | `activeSubject` IDs (subjectItems) |
| `"choose-subject"` | Default/placeholder for `activeSubject` |
| `"choose-chapter"` | Default/placeholder for `activeChapter` |
### Data flow
```
User clicks grade in SelectorFlow
→ handleGradeSelect(item) → setActiveGrade(item.label) → pill shows label
User clicks subject in SelectorFlow
→ handleSubjectSelect(item) → setActiveSubject(item.id) → "math"
→ getSubjectLabel("math") → "Mathematics" (for pill/placeholder display)
→ handleChatSelect restores: data.session.subject = "math" → matches subject.id
User clicks subject in Sidebar
→ onSubjectChange(id) → setActiveSubject(id) → resets selectorStep to "subject"
User clicks subject in TopNav dropdown
→ selectOption("subject", subject.id) → onSubjectChange(id)
→ setActiveSubject(id) → subject-aware chapter dropdown updates
User clicks grade/chapter in TopNav
→ selectOption("grade", grade.label) / selectOption("chapter", chapter.label)
→ directly updates state
```
### Subject label resolution (central pattern)
```
activeSubject (stores ID) = "math"
→ getSubjectLabel("math") → "Mathematics"
→ TopNav pill: resolvedSubjectLabel
→ ChatInput placeholder: `${getSubjectLabel(activeSubject)}`
→ SelectorFlow subtitle: `Subject – ${getSubjectLabel(activeSubject)}`
→ TopNav dropdown: activeSubject === subject.id → highlights Mathematics
→ Sidebar: activeSubject === subject.id → highlights Mathematics
```
### Chapter label resolution
```
activeChapter (stores ID/label) = "choose-chapter" → getChapterLabel("choose-chapter") → "Choose Chapter"
activeChapter (stored label) = "Chapter 1: Rational Numbers" → getChapterLabel(...) → "Chapter 1: Rational Numbers"
→ TopNav pill: resolvedChapterLabel
→ ChatInput placeholder: `${getChapterLabel(activeChapter)}`
```
### TopNav chapter dropdown (subject-aware)
```
chapterData[activeSubject] → chapterOptions array
→ ["Choose Chapter", ...chaptersFromSubject]
→ Changes dynamically when activeSubject changes
→ Previously: static list mixing chapters from all subjects
```
### How grade is used
- Sent in `POST /api/chat` body → `grade` field (only when not default)
- Used in AI system prompt via `buildSystemPrompt()` → `"You are teaching students in grade X"`
- Displayed in chat input placeholder: `"Ask anything about {grade} {subject} – {chapter}..."`
- Restored when loading a session from `data.session.grade`
### SelectorFlow component
- Generic: accepts `title`, `subtitle`, `items`, `selectedValue`, `onSelect`, `step`, `compareField` props
- `compareField` determines which property to compare against `selectedValue` for active highlighting:
- `"label"` → `selectedValue === item.label` (used for grades and chapters)
- `"id"` → `selectedValue === item.id` (used for subjects)
- Renders any item type via 3-column grid with fade-in animation
- Conditional icon rendering: `item.icon && (icon)`
- Fade-in animation on step change via `key={step}` — CSS `@keyframes selectorFadeIn` with `opacity` + `translateY(16px→0)` over 0.35s
### Selector data
- **Grades**: 7 options (Grade 6–12), each with icon (Material Symbols)
- **Subjects**: 6 options (Math, Science, History, Language, English, Geography) — label only, no icons
- **Chapters**: 6 per subject, subject-aware via `chapterData` object in App.jsx — label only, no icons
- **Chapter mapping**: `chapterData[subjectId]` → array of chapters (e.g. math → Rational Numbers, Linear Equations, etc.)
### Current features ✅
- Sequential selector flow: grade → subject → chapter → chat (fade-in transitions)
- Grade selection via SelectorFlow (new-chat grid)
- Grade selection via TopNav (header pill dropdown)
- Subject selection in SelectorFlow (label-only, no icons)
- Chapter selection in SelectorFlow (subject-aware, label-only, no icons)
- Grade persists across session restore
- Grade sent to AI backend for context-aware responses
- 7 grades available (Grade 6–12)
- Upgrade-to-Pro card hidden via `showUpgrade={false}` prop
---
## Feature Tracker
### Grade Selector
- **Status**: Working ✅
- **Verified**: Yes — hover effect confirmed
- **Last updated**: 2026-01-18
### Hover effect (✅ verified)
- Subtle lift: `translateY(-2px)`
- Box-shadow pop via `--shadow-card-hover`
- Smooth transition on transform and shadow only
- Press-down feel on `:active` (`translateY(0)`)
- File: `frontend/src/components/selector-flow/SelectorFlow.css`
### Sequential selector flow (✅ verified)
- Steps: grade → subject → chapter → chat (via `selectorStep` state)
- Fade-in animation on each step change: `opacity 0→1` + `translateY(16px→0)` over 0.35s
- Triggered by `key={step}` on `.selector-flow` — forces React re-mount
- Subject-aware chapters: `chapterData[subjectId]` returns relevant chapters
- File: `frontend/src/App.jsx` (selector data + step logic), `frontend/src/components/chat-history/ChatHistory.jsx` (conditional rendering)
### Subject ID/label resolution pattern (✅ verified in browser)
- `activeSubject` now stores **IDs** (e.g. `"math"`, `"science"`) instead of labels
- Default is `"choose-subject"` (a reserved placeholder ID)
- `getSubjectLabel(id)` resolves IDs to display names: `"math"` → `"Mathematics"`
- Used in: TopNav pill text, ChatInput placeholder, SelectorFlow subtitle, `handleSend` body
- `handleSubjectSelect` stores `item.id` (not `item.label`)
- `handleChatSelect` restoration: backend returns `subject: "math"` which matches `subject.id`
- **TopNav**: uses `subjectItems` prop; `activeSubject === subject.id` comparison works; pill uses `resolvedSubjectLabel`
- **TopNav chapter dropdown**: uses `chapterData` prop; generates `chapterOptions[activeSubject]` dynamically (subject-aware)
- **Sidebar**: `activeSubject === subject.id` comparison now works correctly; sidebar subject change resets selector to `subject` step
- **SelectorFlow**: `compareField="id"` for subjects, `compareField="label"` for grades/chapters
- **ChatInput placeholder**: uses `getSubjectLabel(activeSubject)` and `getChapterLabel(activeChapter)` for full label resolution
- Files: `App.jsx`, `TopNav.jsx`, `Sidebar.jsx`, `SelectorFlow.jsx`, `ChatInput.jsx`
### Chapter pill resolution (✅ verified in browser)
- `activeChapter` stores chapter labels (e.g. `"Chapter 1: Rational Numbers"`) after selection
- Default is `"choose-chapter"` (placeholder ID)
- `getChapterLabel(chapterId)` resolves placeholder: `"choose-chapter"` → `"Choose Chapter"`
- Used in: TopNav pill, ChatInput placeholder
- Files: `App.jsx`, `TopNav.jsx`
### TopNav subject-aware chapter dropdown (✅ verified in browser)
- Replaced static 6-item chapters array in TopNav with dynamic `chapterOptions` generated from `chapterData[activeSubject]`
- Shows subject-specific chapters based on current selection:
- Math → Rational Numbers, Linear Equations, Coordinate Geometry, Algebra, Geometry, Trigonometry
- Science → Nutrition in Plants, Photosynthesis, Human Physiology, etc.
- (all 6 subjects × 6 chapters)
- Dropdown placeholder highlights correctly when `activeChapter` is `"choose-chapter"`
- Files: `TopNav.jsx` (chapterOptions generation, chapterData prop), `App.jsx` (chapterData pass-through)
### Upgrade-to-Pro card hidden (✅ verified)
- `Sidebar` component accepts `showUpgrade` prop (default `true`)
- `App.jsx` passes `showUpgrade={false}`
- Card is wrapped in `{showUpgrade && (...)}` conditional
- Can be re-enabled by setting `showUpgrade={true}`
- File: `frontend/src/components/sidebar/Sidebar.jsx`, `frontend/src/App.jsx`
---
## Supabase
### Installation
Supabase CLI (local dev via `npx supabase start`).
- **Project path**: `supabase/` (CLI-managed)
- **Start**: `npx supabase start`
- **Stop**: `npx supabase stop`
- **Status**: `npx supabase status`
- **Services**: Postgres, GoTrue (Auth), PostgREST (REST), Storage, Supavisor (pooler), Realtime, Edge Runtime, Studio (dashboard), Kong (API gateway), Mailpit (email testing), Analytics (Logflare), Vector (logging)
### Dashboard (Studio)
| Setting | Value |
|---|---|
| **URL** | `http://localhost:54323` |
| **Mailpit** | `http://localhost:54324` |
### API Keys (local dev)
| Key | Value |
|---|---|
| **Publishable Key** (client-side) | `sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH` |
| **Secret Key** (server-side) | `sb_secret_N7UND0UgjKTVK-Uodkm0Hg_xSvEMPvz` |
> **Never expose the Secret Key in client code.** Use the publishable key in frontend, secret key in backend.
> Local dev uses shared default keys — **do not use in production**.
### Database
| Setting | Value |
|---|---|
| **Host** | `localhost` |
| **Port** | `54322` |
| **Database** | `postgres` |
| **User** | `postgres` |
| **Password** | `postgres` |
**Connection string:**
```bash
psql 'postgres://postgres:postgres@localhost:54322/postgres'
```
### API Endpoints (via Kong gateway)
| Service | Endpoint |
|---|---|
| **REST** | `http://localhost:54321/rest/v1/` |
| **Auth** | `http://localhost:54321/auth/v1/` |
| **Storage** | `http://localhost:54321/storage/v1/` |
| **Realtime** | `http://localhost:54321/realtime/v1/` |
| **GraphQL** | `http://localhost:54321/graphql/v1` |
| **Edge Functions** | `http://localhost:54321/functions/v1/` |
| **MCP** | `http://localhost:54321/mcp` |
### Supabase MCP
- **Endpoint**: `http://localhost:54321/mcp`
- **Mode**: Read-only (local dev)
- **Auth**: Local (no cloud OAuth)
- **Tools**: 10 (database, debugging, development, docs)
- **Config**: `~/.pi/agent/supabase.json` (callback port 54326 — default 54324 conflicts with Mailpit)
- **CLI**: `pi install npm:pi-supabase` → then `supabase_mcp_connect()`
### Database State
- **Custom tables**: `profiles`, `sessions`, and `messages` exist in the connected local Supabase database
- **Auth tables**: Supabase-managed (`auth.users`, `auth.sessions`, `auth.refresh_tokens`, etc.)
- **RLS**: Enabled on custom tables and auth tables
- **Permissions (FIXED)**: API roles lacked table grants, causing PostgreSQL `42501 permission denied`. Fixed via grant to `anon`/`authenticated`/`service_role`; tracked in `supabase/migrations/20260819120000_grant_api_role_table_access.sql`, which also sets `ALTER DEFAULT PRIVILEGES` so future tables inherit the grants.
- **Profiles (control)**: `profiles` has `role` (default `user`) and `is_banned` (default `false`) columns. A trigger on `auth.users` auto-creates a profile row on any user creation (signup or admin). `authenticated` may update only `display_name`/`avatar_url` — it cannot change `role`/`is_banned`; `service_role` has full control. Migration: `supabase/migrations/20260819130000_profiles_control_and_trigger.sql`.
- **Signups**: enabled (email verification not yet wired to the app).
- **User data**: all previous test accounts were deleted (2026-08-19). `auth.users`/`profiles` are empty; no admin user yet. Admin user is added manually in Studio (Auth → Add user, email_confirm on) and then promoted to `role='admin'`.
- **Verified**: self-signup and admin-created users both auto-create a `profiles` row with `role:'user'`; the role is_banned self-edit is blocked at the column-privilege level.
### Environment Files
- `supabase/config.toml` — CLI project config
- `supabase/.temp/` — CLI-generated files (do not commit)
---
## Global Design Tokens (global.css)
| Token | Value |
|---|---|
| `--color-primary` | `#000000` |
| `--color-on-primary` | `#ffffff` |
| `--color-background` | `#fcf8fa` |
| `--color-surface-lowest` | `#ffffff` |
| `--color-on-surface` | `#111827` |
| `--color-on-surface-variant` | `#6b7280` |
| `--font-heading` | `"Montserrat", sans-serif` |
| `--font-body` | `"Inter", system-ui, sans-serif` |
| `--sidebar-width` | `280px` |
| `--container-max` | `800px` |
---
## Recent Implementation
### Backend Authentication (completed 2026-08-18)
The sign-in/sign-out flow was moved from the frontend (direct Supabase Auth calls) to the backend (cookie-based proxy).
#### What was changed
| File | Change |
|---|---|
| `backend/src/routes/auth.js` | New — `POST /api/auth/signup`, `/signin`, `/signout`; `GET /api/auth/me` |
| `backend/src/middleware/supabaseAuth.js` | Updated — reads JWT from `padhle.token` httpOnly cookie instead of `Authorization` header |
| `backend/src/index.js` | Updated — added `cookie-parser` middleware; applied `supabaseAuth` to `/api/chat` and `/api/sessions` |
| `backend/src/stores/sessionStore.js` | Updated — stores `userId` per session; `listSessions(userId)` filters by user |
| `frontend/src/lib/auth/backendAuth.js` | New — frontend auth client (`signup`, `signin`, `signout`, `me`, `check`) |
| `frontend/src/lib/auth/SupabaseAuth.jsx` | Updated — removed all `supabase.auth.*` calls; now uses backend client |
#### How it works
```
1. User enters email + password → Frontend POST /api/auth/signin
2. Backend calls Supabase Auth API → receives JWT
3. Backend sets httpOnly cookies:
- padhle.token = JWT (for API verification)
- padhle.user = JSON({ uid, email }) (safe user metadata)
4. Frontend never sees the JWT — browser stores cookies automatically
5. All API calls include cookies via `credentials: 'include'`
6. Backend middleware reads `padhle.token`, verifies via `/auth/v1/user`
7. If valid, `req.user = { uid, email }` is attached to request
```
#### Verification
- ✅ Build passes (Vite 6.4.3)
- ✅ Sign-in → user authenticated, sees main app
- ✅ Sign-out → cookies cleared, back to sign-in modal
- ✅ Protected routes (`/api/sessions`) properly require auth (401 on missing cookie)
- ✅ `padhle.token` cookie confirmed as `HttpOnly` in browser DevTools
- ✅ No `localStorage` tokens used anywhere in frontend
---
## Security Audit — Vulnerabilities & Fixes
An audit was performed on all backend files on 2026-08-18.
### 🔴 Critical — Fixed
#### CVE-2026-011: Cross-user identity leak via truncated token cache key
**File:** `backend/src/middleware/supabaseAuth.js`
**Problem:** `verifyToken` cached verified users keyed by `token.substring(0, 50)`. Every JWT from the same GoTrue instance shares the header (including `kid`) and the opening claims, so users A and B produced **identical 50-char fingerprints**. B's request then hit A's cached entry and `req.user.uid` resolved to A — so B's `/api/sessions` returned A's sessions/history. Reproduced end-to-end: two fresh users, A created a session, B listed A's session.
**Fix:** Key the cache by the **full token**, which is unique per user:
```js
const cached = tokenCache.get(token); // full token
...
tokenCache.set(token, { ...result, expires }); // full token
```
Removed the `FINGERPRINT_LEN` constant. `listSessions`/RLS filtering was already correct; the leak was purely identity resolution.
**Verified:** On fixed code, A has 1 session, B has 0; no cross-user leak. (Note: the running backend on :3001 must be restarted to load the fix — it runs `node src/index.js` without `--watch`.)
#### CVE-2026-001: IDOR on `GET /api/chat/:chatId`
**File:** `backend/src/routes/chat.js`
**Problem:** The route checks `supabaseAuth` middleware (so user is authenticated), but never verifies the session belongs to `req.user.uid`. Any authenticated user can read another user's conversation history by guessing a `chatId`.
**Fix:** Add ownership check before returning session data:
```js
router.get("/:chatId", (req, res) => {
const session = getSession(req.params.chatId);
if (!session || session.userId !== req.user.uid) {
return res.status(404).json({ error: "Session not found" });
}
// ... rest of route
});
```
Also apply to `GET /api/chat/sessions` to filter by `req.user.uid`.
---
#### CVE-2026-002: No rate limiting on auth endpoints
**File:** `backend/src/routes/auth.js`
**Problem:** Zero throttling on signup/signin/signout. Allows unlimited password brute-force, account enumeration via signup attempts, and session flooding.
**Fix:** Apply `express-rate-limit` to auth routes:
```js
import rateLimit from 'express-rate-limit';
const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 20 });
router.post('/signup', authLimiter, ...);
router.post('/signin', authLimiter, ...);
router.post('/signout', authLimiter, ...);
```
---
#### CVE-2026-003: SSE stream injection — untrusted data in `data: ` prefix
**File:** `backend/src/routes/chat.js`
**Problem:** User messages (AI responses built from user input) are written raw into SSE events:
```js
res.write(`data: ${JSON.stringify({ type: "chunk", content: chunk })}\n\n`);
```
If `chunk` contains `}\n\n` or other control characters, it breaks the SSE stream and can inject fake events.
**Fix:** Sanitize the JSON output before writing:
```js
const safe = JSON.stringify({ type: "chunk", content: chunk })
.replace(/\n/g, '\\n')
.replace(/\u2028/g, '\\u2028')
.replace(/\u2029/g, '\\u2029');
res.write(`data: ${safe}\n\n`);
```
---
#### CVE-2026-004: Google Gemini API URL path break
**File:** `backend/src/services/ai.js`
**Problem:** API key concatenated into URL path — if the key contains `/` or `..`, the URL breaks:
```js
const url = `${GOOGLE_BASE_URL}${apiKey}/chat/models/...`;
// = https://generativelanguage.googleapis.com/v1beta/openai/{key}/chat/... ← wrong
```
**Fix:** Use `URL` constructor to build safely:
```js
const url = new URL(`chat/models/${model}:streamGenerateContent`, GOOGLE_BASE_URL);
url.searchParams.set('key', apiKey);
```
---
### 🟡 High — Fixes Applied
#### CVE-2026-005: Supabase error details leaked to client
**File:** `backend/src/routes/auth.js`
**Problem:** `data.error_description` from Supabase returns detailed error text (e.g., "Email already registered") that reveals system behavior.
**Fix:** Return generic messages from the backend; log raw errors server-side only:
```js
// Log the real error
console.error(`Auth error: ${data.error_description}`);
// Return generic message
return res.status(400).json({ error: "Sign up failed" });
```
---
#### CVE-2026-006: No timeout on Supabase JWT verification HTTP calls
**File:** `backend/src/middleware/supabaseAuth.js`
**Problem:** Every request to a protected route makes an HTTP call to `Supabase /auth/v1/user` with no timeout. If Supabase is slow or unreachable, the Express server hangs indefinitely.
**Fix:** Add `AbortSignal` timeout:
```js
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
const res = await fetch(url, { headers, signal: controller.signal });
clearTimeout(timeout);
```
---
#### CVE-2026-007: No token caching — every request hits Supabase
**File:** `backend/src/middleware/supabaseAuth.js`
**Problem:** Every single request makes an HTTP call to verify the token. A token is valid for 3600s; it should be cached.
**Fix:** Cache verified tokens in a `Map` with TTL:
```js
const tokenCache = new Map(); // tokenFingerprint -> { uid, email, expires }
const FINGERPRINT_TTL = 5 * 60 * 1000;
const TOKEN_EXPIRY_TTL = 3600 * 1000;
export async function verifyToken(token) {
const fp = token.substring(0, 50);
const cached = tokenCache.get(fp);
if (cached && Date.now() < cached.expires) {
return cached;
}
// ... verify via Supabase, then:
tokenCache.set(fp, { uid, email, expires: Date.now() + FINGERPRINT_TTL });
return { uid, email };
}
```
---
### 🟢 Medium — Fixes Applied
#### CVE-2026-008: No input length limits on chat messages
**File:** `backend/src/routes/chat.js`
**Problem:** No max on `text` length. Can fill the in-memory store or blow up the AI context window.
**Fix:** Add length check:
```js
if (!text || !text.trim()) return 400;
if (text.length > 10000) return 400;
```
---
#### CVE-2026-009: No XSS sanitization on stored messages
**File:** `backend/src/stores/sessionStore.js`
**Problem:** User messages stored and returned raw. If the frontend renders them as HTML, user input containing `