diff --git a/CLAUDE.md b/CLAUDE.md index ff4f2bf..011225b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,58 +22,89 @@ app/ page.tsx # entire chat UI — one client component layout.tsx # HTML shell, imports CSS + highlight.js theme globals.css # all styles + error.tsx # error boundary for the main page (client component) + not-found.tsx # custom 404 page login/page.tsx # Google sign-in page (server component) share/[id]/ page.tsx # read-only shared chat view (server component) fork-button.tsx # "continue this chat" button (client component) + opengraph-image.tsx # OG image generated from share data api/ auth/[...nextauth]/route.ts # NextAuth handler chat/route.ts # streaming LLM proxy (OpenAI, Anthropic, Gemini) + health/route.ts # GET — DB connectivity check, for uptime monitors shares/route.ts # POST — create a share shares/[id]/route.ts # GET — fetch a share + history/route.ts # GET — list encrypted chat history; POST — save/update + history/[id]/route.ts # DELETE — remove a history entry + settings/route.ts # GET/PUT — user settings (system prompt, save toggle, key) lib/ chat.ts # pure message-conversion functions (tested) + crypto.ts # AES-GCM encrypt/decrypt for chat history db.ts # pg Pool singleton + query helper + log.ts # pino logger instance (import and use directly) markdown.ts # marked + marked-highlight + highlight.js setup tests/ unit.test.ts # node:test unit tests for lib/chat.ts + lib/markdown.ts -auth.ts # NextAuth config (Google provider, email allowlist) -middleware.ts # auth guard for all non-login routes +auth.config.ts # lightweight NextAuth config (edge-safe, no providers) +auth.ts # full NextAuth config (Google provider) — server only +middleware.ts # auth guard using auth.config.ts (keeps middleware bundle small) scripts/migrate.mjs # one-shot DB table creation ``` ## Dependencies -Runtime: `next`, `react`, `react-dom`, `next-auth`, `marked`, `marked-highlight`, `highlight.js`, `@google/genai`, `pg` +Runtime: `next`, `react`, `react-dom`, `next-auth`, `marked`, `marked-highlight`, `highlight.js`, `pg`, `pino` -The `@google/genai` and `pg` packages are in `serverExternalPackages` in `next.config.ts` to avoid webpack bundling issues. +`pg` and `pino` are in `serverExternalPackages` in `next.config.ts` to avoid bundling issues. ## Auth -NextAuth v5 (beta) with Google OAuth. All routes except `/login` and `/api/auth/*` require authentication (enforced in `middleware.ts`). The `ALLOWED_EMAIL` env var is a comma-separated list of permitted emails; anyone else is rejected at the `signIn` callback in `auth.ts`. +NextAuth v5 (beta) with Google OAuth. All routes except `/login`, `/share/*`, `/api/auth/*`, and `/api/health` require authentication (enforced in `middleware.ts`). The `ALLOWED_EMAIL` env var is a comma-separated list of permitted emails; anyone else is rejected at the `signIn` callback in `auth.config.ts`. + +**Important:** `auth.config.ts` is the lightweight edge-safe config used in middleware — it must include `providers: []` even though it has no providers, or next-auth will crash with a `.map()` error on undefined. `auth.ts` imports `auth.config.ts` and adds the Google provider for server-side use. ## LLM providers -`app/api/chat/route.ts` handles all three providers: +`app/api/chat/route.ts` handles all three providers via their REST APIs directly (no SDKs): + +- **OpenAI** — REST SSE, `Authorization: Bearer` header; web search uses the Responses API (`/v1/responses`) +- **Anthropic** — REST SSE, `x-api-key` header, `anthropic-version` header; system prompt is a top-level field; web search uses a multi-turn tool loop (non-streaming rounds, then final text) +- **Google Gemini** — REST SSE via `generativelanguage.googleapis.com`; role `assistant` maps to `model`; system prompt via `systemInstruction` + +Images are stored as `{ data: string, mimeType: string }` (raw base64, no data-URL prefix). PDFs are stored as `{ name: string, data: string }` (base64). Both are converted to provider-specific formats in the route. + +The chat route validates requests before hitting any upstream API: `messages` must be a non-empty array (max 200), and `model` must be in `ALLOWED_MODELS`. + +## Logging -- **OpenAI** — REST SSE, `Authorization: Bearer` header -- **Anthropic** — REST SSE, `x-api-key` header, `anthropic-version` header; system prompt is a top-level field (not a message) -- **Google Gemini** — uses `@google/genai` SDK (`generateContentStream`); role `assistant` maps to `model`; system prompt via `config.systemInstruction` +Use `logger` from `lib/log.ts` (pino). Convention: +- `logger.info(fields, 'event.name')` — normal operations +- `logger.warn(fields, 'event.name')` — auth failures, 4xx +- `logger.error(fields, 'event.name')` — 5xx, exceptions -Images are stored as `{ data: string, mimeType: string }` (raw base64, no data-URL prefix) in the `Message` type and converted to provider-specific formats in the route. +The chat route uses a try/finally to emit one canonical log line per request with: `user`, `model`, `provider`, `msgs`, `webSearch`, `status`, `ms` (time to first stream byte). Set `LOG_LEVEL=debug` in env for more verbose output. + +## Chat history + +Stored encrypted in Postgres (`chat_histories` table). Encryption is AES-GCM (256-bit), done entirely client-side in `lib/crypto.ts` using the Web Crypto API. The key is stored server-side in `user_settings.key_jwk` as a JWK so it's shared across devices/deployments. Only the encrypted `iv` + `ciphertext` blobs are stored in the DB — the server never sees plaintext history. ## Shared chats -Stored in a single Postgres table (`shared_chats`) in Neon. Only explicitly shared chats are stored — normal sessions are ephemeral (in-memory React state). The schema is in `scripts/migrate.mjs`. +Stored in plaintext in `shared_chats` (Postgres/Neon). Only explicitly shared chats are stored — normal sessions are ephemeral React state. The schema is in `scripts/migrate.mjs`. The fork flow is client-side only: the fork button writes `{ messages, model, systemPrompt }` to `localStorage` under `gippidy-fork`, then navigates to `/`. The main page checks for that key on mount, loads it, and clears it. +## Security headers + +Set in `next.config.ts` for all routes: `X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy`. + ## Styles -All in `globals.css`. Retro terminal aesthetic: dark background (`#0c0c0c`), green (`#33ff33`) accents, monospace font, no border-radius. CSS variables are defined in `:root`. Mobile layout uses `100dvh` and a `flex-wrap` breakpoint at 540px for the header. +All in `globals.css`. Retro terminal aesthetic: dark background (`#0c0c0c`), green (`#33ff33`) accents, monospace font, no border-radius. CSS variables are defined in `:root`. Mobile layout uses `100dvh` and a `flex-wrap` breakpoint at 540px for the header. The share page adds a `.share-view` class (lighter border) and a `.share-banner` strip to visually distinguish it from the live chat UI. ## What to avoid @@ -81,3 +112,4 @@ All in `globals.css`. Retro terminal aesthetic: dark background (`#0c0c0c`), gre - Do not add Tailwind or any CSS preprocessor - Do not split `app/page.tsx` into many sub-components unless it becomes unmanageable - Keep the dependency list short — every new package is a future migration burden +- When adding a model to `MODELS` in `app/page.tsx`, also add it to `ALLOWED_MODELS` in `app/api/chat/route.ts` diff --git a/README.md b/README.md index 32b113a..c866f2a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # gippidy -A minimal LLM chat app. Supports OpenAI, Anthropic, and Google Gemini models with streaming responses, markdown rendering, image inputs, and shareable chat sessions. Hosted on Vercel. +A minimal LLM chat app. Supports OpenAI, Anthropic, and Google Gemini models with streaming responses, markdown rendering, image and PDF inputs, web search, encrypted chat history, and shareable chat sessions. Hosted on Vercel. ## Prerequisites @@ -33,6 +33,12 @@ DATABASE_URL=postgres://... OPENAI_API_KEY=sk-... ANTHROPIC_API_KEY=sk-ant-... GOOGLE_GENERATIVE_AI_API_KEY=AIza... + +# Optional: set to debug, info, warn, or error (default: info) +LOG_LEVEL=info + +# Base URL for OG image metadata (defaults to https://www.gippidy.chat) +NEXT_PUBLIC_BASE_URL=https://your-domain.vercel.app ``` ## Google OAuth setup @@ -48,7 +54,7 @@ GOOGLE_GENERATIVE_AI_API_KEY=AIza... ```bash pnpm install -pnpm db:migrate # creates the shared_chats table — run once against your Neon DB +pnpm db:migrate # creates DB tables — run once against your Neon DB pnpm dev ``` @@ -69,10 +75,15 @@ Add your production Vercel URL to the authorized redirect URIs in Google Cloud C - **Multiple models** — GPT-5.4, Claude Opus 4.6, Claude Sonnet 4.6, Gemini 3.1 Pro, Gemini 3 Flash - **Streaming responses** with smart scroll (auto-follows stream unless you scroll up) - **Markdown + syntax highlighting** via marked and highlight.js -- **Image inputs** — attach via file picker, paste from clipboard -- **System prompt** — configurable per-session, persisted in localStorage -- **Shared chats** — generate a shareable URL; recipients must be authenticated to view; they can fork the chat to continue it themselves +- **Image inputs** — attach via file picker, paste from clipboard, or drag and drop +- **PDF inputs** — attach PDFs for models that support document reading (Anthropic, Gemini) +- **File inputs** — attach text/code files; contents are inlined as XML-tagged blocks +- **Web search** — per-request toggle; uses each provider's native search tool +- **System prompt** — configurable, persisted server-side per user +- **Encrypted chat history** — saved chats are AES-GCM encrypted client-side; key is stored server-side so it's shared across devices +- **Shared chats** — generate a shareable read-only URL with OG image preview; authenticated users can fork the chat to continue it - **Google OAuth** — restricted to a configurable allowlist of emails +- **Health endpoint** — `GET /api/health` checks DB connectivity; suitable for uptime monitors ## Scripts @@ -81,4 +92,5 @@ Add your production Vercel URL to the authorized redirect URIs in Google Cloud C | `pnpm dev` | Start development server | | `pnpm build` | Production build | | `pnpm start` | Start production server | +| `pnpm test` | Run unit tests | | `pnpm db:migrate` | Create database tables (run once) |