# Corporate AI Chat Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Build a ChatGPT-style corporate chat UI with one-day header-based JWT authentication, local chat history, three model routes, and a single streaming AI Gateway adapter. **Architecture:** A client-side auth gate validates a JWT from `localStorage` against protected Next.js Route Handlers before rendering the chat. Server-only auth and gateway modules normalize security, validation, and streaming while focused React components own the responsive chat UI and device-local state. **Tech Stack:** Next.js 16.2, React 19.2, TypeScript 5, Tailwind CSS 4, shadcn/Base UI, Lucide React, `jose`, Vitest, React Testing Library, JSDOM. ## Global Constraints - Use one `GatewayAdapter`; provider-specific logic must not exist in this application. - Supported route IDs are exactly `chatgpt`, `deepseek`, and `qwen`. - JWT is HS256, sent only as `Authorization: Bearer `, and expires exactly 24 hours after issue. - JWT is persisted under `ai-control-center:auth-token`; do not use cookies. - Test credentials are `demo@ai-control.local` and `Demo1234!`, supplied through server-only environment variables. - The user JWT must never be forwarded to AI Gateway. - Chat history is device-local and namespaced by authenticated user ID. - Render model output as plain text with preserved line breaks; never inject raw HTML. - A request allows at most 100 messages, 32,000 characters per message, and 128,000 characters total. - Preserve the existing Next.js, npm, shadcn, and Tailwind structure. --- ## File Structure - `lib/auth/types.ts`: serializable public user identity shared by server and client. - `lib/auth/server.ts`: JWT signing, verification, credentials comparison, and Bearer extraction. - `lib/auth/client.ts`: browser token persistence and authenticated fetch helper. - `app/api/auth/login/route.ts`: credential exchange for a one-day JWT. - `app/api/auth/me/route.ts`: protected identity probe used during startup. - `components/auth/auth-gate.tsx`: startup auth state machine and login/chat switching. - `components/auth/login-screen.tsx`: accessible corporate login form. - `lib/chat/types.ts`: shared model, message, conversation, and request types. - `lib/chat/models.ts`: model display configuration only. - `lib/chat/storage.ts`: validated, user-namespaced `localStorage` persistence. - `lib/chat/validation.ts`: runtime validation and request limits. - `lib/chat/gateway.ts`: single HTTP/mock streaming adapter. - `app/api/chat/route.ts`: authenticated validated streaming endpoint. - `hooks/use-chat.ts`: chat lifecycle, streaming, cancellation, retry, and persistence. - `components/chat/*`: focused sidebar, header, empty state, message list, and composer views. - `components/ui/input.tsx`, `components/ui/textarea.tsx`: missing shadcn form primitives. - `app/page.tsx`: thin entry point that renders the auth gate. - `app/globals.css`: product visual system and responsive base behavior. - `app/layout.tsx`: Russian metadata and document language. - `next.config.ts`: CSP and security response headers. --- ### Task 1: Test Harness and Server Authentication **Files:** - Modify: `package.json` - Modify: `package-lock.json` - Modify: `.gitignore` - Create: `.env.example` - Create locally, do not commit: `.env.local` - Create: `vitest.config.mts` - Create: `vitest.setup.ts` - Create: `lib/auth/types.ts` - Create: `lib/auth/server.ts` - Create: `lib/auth/server.test.ts` - Create: `app/api/auth/login/route.ts` - Create: `app/api/auth/me/route.ts` - Create: `app/api/auth/routes.test.ts` **Interfaces:** - Produces: `AuthIdentity`, `VerifiedAuthToken`, `signAuthToken(user)`, `verifyAuthToken(token)`, `authenticateRequest(request)`, and JSON auth routes. - Consumes: server-only `AUTH_USER_ID`, `AUTH_EMAIL`, `AUTH_PASSWORD`, and `JWT_SECRET`. - [ ] **Step 1: Install auth and test dependencies and add scripts** Run: ```bash npm install jose server-only npm install --save-dev vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/dom @testing-library/jest-dom @testing-library/user-event vite-tsconfig-paths ``` Add scripts: ```json "test": "vitest run", "test:watch": "vitest" ``` - [ ] **Step 2: Configure Vitest and environment templates** Create `vitest.config.mts`: ```ts import react from "@vitejs/plugin-react"; import { defineConfig } from "vitest/config"; import tsconfigPaths from "vite-tsconfig-paths"; export default defineConfig({ plugins: [tsconfigPaths(), react()], test: { environment: "jsdom", setupFiles: ["./vitest.setup.ts"], clearMocks: true, }, }); ``` Create `vitest.setup.ts`: ```ts import "@testing-library/jest-dom/vitest"; ``` Add `!.env.example` after `.env*` in `.gitignore`. Commit this template: ```dotenv AUTH_USER_ID=demo-user AUTH_EMAIL=demo@ai-control.local AUTH_PASSWORD=change-me JWT_SECRET=replace-with-at-least-32-random-characters AI_GATEWAY_MOCK=true AI_GATEWAY_URL= AI_GATEWAY_API_KEY= ``` Create the ignored `.env.local` for immediate local use: ```dotenv AUTH_USER_ID=demo-user AUTH_EMAIL=demo@ai-control.local AUTH_PASSWORD=Demo1234! JWT_SECRET=ai-control-center-local-development-secret-2026 AI_GATEWAY_MOCK=true ``` - [ ] **Step 3: Write failing JWT and Bearer tests** Create tests that pin the public API: ```ts it("issues an HS256 token for exactly 24 hours", async () => { const token = await signAuthToken({ id: "demo-user", email: "demo@ai-control.local" }); const user = await verifyAuthToken(token); expect(user).toMatchObject({ id: "demo-user", email: "demo@ai-control.local" }); expect(user.exp - user.iat).toBe(86_400); }); it("rejects missing and malformed bearer authorization", async () => { await expect(authenticateRequest(new Request("http://localhost/api/chat"))).rejects.toMatchObject({ status: 401 }); await expect(authenticateRequest(new Request("http://localhost/api/chat", { headers: { Authorization: "Token invalid" }, }))).rejects.toMatchObject({ status: 401 }); }); ``` - [ ] **Step 4: Run the auth tests and verify failure** Run: `npm test -- lib/auth/server.test.ts` Expected: FAIL because `lib/auth/server.ts` does not exist. - [ ] **Step 5: Implement the server-only auth module** Use `jose` with an explicit algorithm allowlist and stable claims: ```ts import "server-only"; import { SignJWT, jwtVerify } from "jose"; import type { AuthIdentity } from "@/lib/auth/types"; export type VerifiedAuthToken = AuthIdentity & { iat: number; exp: number }; export async function signAuthToken(user: AuthIdentity): Promise { return new SignJWT({ email: user.email }) .setProtectedHeader({ alg: "HS256", typ: "JWT" }) .setSubject(user.id) .setIssuedAt() .setExpirationTime("24h") .sign(new TextEncoder().encode(requireEnv("JWT_SECRET"))); } export async function verifyAuthToken(token: string): Promise { const { payload } = await jwtVerify(token, new TextEncoder().encode(requireEnv("JWT_SECRET")), { algorithms: ["HS256"], }); if (!payload.sub || typeof payload.email !== "string" || !payload.iat || !payload.exp) throw unauthorized(); return { id: payload.sub, email: payload.email, iat: payload.iat, exp: payload.exp }; } ``` Define `AuthIdentity` as `{ id: string; email: string }` in `lib/auth/types.ts`. Add `credentialsMatch(email, password)` using constant-time string comparisons and `authenticateRequest(request)` requiring the exact case-insensitive `Bearer` scheme with a non-empty token. Normalize all verification failures to an `AuthError` with status `401`. - [ ] **Step 6: Write failing route tests** Cover successful login, generic invalid-credentials response, malformed JSON, and `/api/auth/me` with a valid/invalid Bearer token: ```ts const response = await login(new Request("http://localhost/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: "demo@ai-control.local", password: "Demo1234!" }), })); expect(response.status).toBe(200); expect(await response.json()).toMatchObject({ token: expect.any(String), user: { id: "demo-user" } }); ``` - [ ] **Step 7: Implement login and identity Route Handlers** `POST /api/auth/login` returns `{ token, user: { id, email } }` on success, `{ error: "Неверная почта или пароль" }` with `401` on mismatch, and `{ error: "Некорректный запрос" }` with `400` on invalid JSON. `GET /api/auth/me` returns `{ user: { id, email } }` after `authenticateRequest` and a generic `401` otherwise. Both responses use `Cache-Control: no-store`. - [ ] **Step 8: Run auth tests and commit** Run: `npm test -- lib/auth/server.test.ts app/api/auth/routes.test.ts` Expected: PASS. Commit: ```bash git add package.json package-lock.json .gitignore .env.example vitest.config.mts vitest.setup.ts lib/auth app/api/auth git commit -m "feat: add header-based JWT authentication" ``` --- ### Task 2: Auth Bootstrap and Login Screen **Files:** - Create: `lib/auth/client.ts` - Create: `components/auth/login-screen.tsx` - Create: `components/auth/auth-gate.tsx` - Create: `components/auth/auth-gate.test.tsx` - Create: `components/chat/chat-app.tsx` as the authenticated shell, expanded in Task 5 - Create: `components/ui/input.tsx` - Modify: `app/page.tsx` **Interfaces:** - Consumes: `GET /api/auth/me`, `POST /api/auth/login`, `AuthIdentity`. - Produces: `AuthGate`, `authFetch(input, init)`, `getStoredToken()`, `clearStoredToken()`. - [ ] **Step 1: Write failing auth-gate tests** Use fake `localStorage` and mocked `fetch` to pin three states: ```tsx it("never renders login while a stored token is being validated", async () => { localStorage.setItem(AUTH_TOKEN_KEY, "valid-token"); vi.mocked(fetch).mockReturnValue(new Promise(() => undefined)); render(); expect(screen.getByLabelText("Проверяем доступ")).toBeInTheDocument(); expect(screen.queryByRole("heading", { name: "Вход" })).not.toBeInTheDocument(); }); it("opens the chat after a valid identity response", async () => { localStorage.setItem(AUTH_TOKEN_KEY, "valid-token"); vi.mocked(fetch).mockResolvedValue(jsonResponse({ user: { id: "demo-user", email: "demo@ai-control.local" } })); render(); expect(await screen.findByText("Новый чат")).toBeInTheDocument(); }); ``` Also verify absent/invalid tokens show login, successful login stores the token, every identity request includes `Authorization: Bearer valid-token`, and logout removes it. - [ ] **Step 2: Run the auth-gate tests and verify failure** Run: `npm test -- components/auth/auth-gate.test.tsx` Expected: FAIL because the auth client and components do not exist. - [ ] **Step 3: Implement browser token helpers** Create a focused module: ```ts export const AUTH_TOKEN_KEY = "ai-control-center:auth-token"; export function getStoredToken() { return window.localStorage.getItem(AUTH_TOKEN_KEY); } export async function authFetch(input: RequestInfo | URL, init: RequestInit = {}) { const token = getStoredToken(); const headers = new Headers(init.headers); if (token) headers.set("Authorization", `Bearer ${token}`); return fetch(input, { ...init, headers }); } ``` Also implement `storeToken` and `clearStoredToken` without accessing `window` at module initialization. - [ ] **Step 4: Implement the login screen and auth state machine** `AuthGate` is a client component with `checking`, `anonymous`, and `authenticated` states. On mount it validates any stored token through `authFetch("/api/auth/me", { cache: "no-store" })`; `401` clears storage. Successful login stores the returned token and user. Logout clears the token and in-memory user. `LoginScreen` uses labeled email/password inputs, `autoComplete`, submit loading state, disabled empty submit, and an `aria-live="polite"` generic error. The heading is `Вход`, the product name is `AI Control Center`, and the primary action is `Продолжить`. Create the initial `ChatApp({ user, onLogout })` authenticated shell with the visible `Новый чат` label and a working logout action. Task 5 replaces its body with the complete chat experience without changing its public props. - [ ] **Step 5: Run tests and commit** Run: `npm test -- components/auth/auth-gate.test.tsx` Expected: PASS. Commit: ```bash git add lib/auth/client.ts components/auth components/chat/chat-app.tsx components/ui/input.tsx app/page.tsx git commit -m "feat: add persistent login experience" ``` --- ### Task 3: Chat Domain and User-Scoped Persistence **Files:** - Create: `lib/chat/types.ts` - Create: `lib/chat/models.ts` - Create: `lib/chat/storage.ts` - Create: `lib/chat/storage.test.ts` **Interfaces:** - Produces: `ModelId`, `ChatMessage`, `Conversation`, `ChatRequest`, `MODELS`, `loadConversations(userId)`, `saveConversations(userId, conversations)`. - Consumes: authenticated `user.id` from `AuthGate`. - [ ] **Step 1: Write failing storage and model tests** ```ts it("namespaces conversations by authenticated user", () => { saveConversations("user-a", [conversation]); expect(loadConversations("user-a")).toEqual([conversation]); expect(loadConversations("user-b")).toEqual([]); }); it("discards malformed persisted data", () => { localStorage.setItem(storageKey("demo-user"), "{not-json"); expect(loadConversations("demo-user")).toEqual([]); }); it("contains only gateway route identifiers", () => { expect(MODELS.map((model) => model.id)).toEqual(["chatgpt", "deepseek", "qwen"]); }); ``` - [ ] **Step 2: Run the domain tests and verify failure** Run: `npm test -- lib/chat/storage.test.ts` Expected: FAIL because the chat domain modules do not exist. - [ ] **Step 3: Implement types and model display configuration** ```ts export type ModelId = "chatgpt" | "deepseek" | "qwen"; export type MessageRole = "user" | "assistant"; export type MessageStatus = "streaming" | "complete" | "error" | "stopped"; export type ChatMessage = { id: string; role: MessageRole; content: string; createdAt: string; status: MessageStatus; }; export type Conversation = { id: string; title: string; modelId: ModelId; updatedAt: string; messages: ChatMessage[]; }; ``` Create `MODELS` with Russian descriptions: ChatGPT as a universal assistant, DeepSeek for reasoning/code, and Qwen for multilingual work. Do not include provider API endpoints or concrete upstream model versions. - [ ] **Step 4: Implement defensive local storage** Use `ai-control-center:chats:${encodeURIComponent(userId)}`. Parse unknown JSON with explicit shape checks for IDs, roles, statuses, dates, and supported model IDs. Sort loaded conversations newest-first, cap persistence at 50 conversations, and return `[]` on browser storage exceptions. - [ ] **Step 5: Run tests and commit** Run: `npm test -- lib/chat/storage.test.ts` Expected: PASS. Commit: ```bash git add lib/chat git commit -m "feat: add local chat domain" ``` --- ### Task 4: Single Streaming AI Gateway Adapter **Files:** - Create: `lib/chat/validation.ts` - Create: `lib/chat/validation.test.ts` - Create: `lib/chat/gateway.ts` - Create: `lib/chat/gateway.test.ts` - Create: `app/api/chat/route.ts` - Create: `app/api/chat/route.test.ts` **Interfaces:** - Consumes: `ChatRequest`, `authenticateRequest(request)`, `AI_GATEWAY_URL`, `AI_GATEWAY_API_KEY`, and `AI_GATEWAY_MOCK`. - Produces: `validateChatRequest(value)`, `streamGatewayResponse(request, signal)`, authenticated `POST /api/chat` text stream. - [ ] **Step 1: Write failing request validation tests** Cover the happy path and each exact limit: ```ts expect(validateChatRequest({ model: "chatgpt", messages: [{ role: "user", content: "Привет" }] })).toEqual({ model: "chatgpt", messages: [{ role: "user", content: "Привет" }], }); expect(() => validateChatRequest({ model: "other", messages: [] })).toThrow(ChatValidationError); expect(() => validateChatRequest({ model: "qwen", messages: Array.from({ length: 101 }, () => ({ role: "user", content: "x" })) })).toThrow(ChatValidationError); ``` Also test a 32,001-character message and a 128,001-character total. - [ ] **Step 2: Run validation tests and verify failure** Run: `npm test -- lib/chat/validation.test.ts` Expected: FAIL because validation is absent. - [ ] **Step 3: Implement strict validation** Reject non-objects, unknown models, empty arrays, unsupported roles, empty/whitespace-only content, more than 100 messages, more than 32,000 characters in one message, and more than 128,000 total characters. Return a new normalized object containing only `model`, `role`, and `content`. - [ ] **Step 4: Write failing gateway tests** ```ts it("sends one normalized request to the configured gateway", async () => { process.env.AI_GATEWAY_URL = "https://gateway.example/v1/chat"; process.env.AI_GATEWAY_API_KEY = "gateway-secret"; vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response("answer"))); const stream = await streamGatewayResponse(request, new AbortController().signal); expect(fetch).toHaveBeenCalledWith("https://gateway.example/v1/chat", expect.objectContaining({ method: "POST", headers: expect.objectContaining({ Authorization: "Bearer gateway-secret" }), body: JSON.stringify(request), })); expect(await new Response(stream).text()).toBe("answer"); }); ``` Also verify the outgoing request never contains the user's JWT, forwards the provided `AbortSignal`, returns deterministic chunks in mock mode, emits `503` when unconfigured, and normalizes non-2xx gateway responses without leaking their body. - [ ] **Step 5: Implement the one-adapter transport** Define `GatewayError` with safe `status` and `message`. `streamGatewayResponse(request, signal)` selects HTTP when `AI_GATEWAY_URL` is present, otherwise mock only when `AI_GATEWAY_MOCK === "true"`. HTTP sends JSON and an optional gateway key. Mock mode returns a UTF-8 `ReadableStream` whose Russian response mentions the selected display model and echoes no secrets; its timers stop when the signal aborts. - [ ] **Step 6: Write failing chat route tests** Verify `401` is returned before body validation, `400` for invalid authenticated payloads, `503` for missing transport, and a successful response has `Content-Type: text/plain; charset=utf-8`, `Cache-Control: no-store`, and streamed text. - [ ] **Step 7: Implement the protected chat route** ```ts export async function POST(request: Request) { try { await authenticateRequest(request); const chatRequest = validateChatRequest(await request.json()); const stream = await streamGatewayResponse(chatRequest, request.signal); return new Response(stream, { headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", }, }); } catch (error) { return chatErrorResponse(error); } } ``` Ensure JSON parse failures become `400`, auth errors remain `401`, configuration/gateway errors are safe `503`/`502`, and unknown errors return generic `500`. - [ ] **Step 8: Run gateway tests and commit** Run: `npm test -- lib/chat/validation.test.ts lib/chat/gateway.test.ts app/api/chat/route.test.ts` Expected: PASS. Commit: ```bash git add lib/chat app/api/chat git commit -m "feat: add streaming AI Gateway adapter" ``` --- ### Task 5: Chat Experience and Streaming State **Files:** - Create: `hooks/use-chat.ts` - Create: `hooks/use-chat.test.tsx` - Modify: `components/chat/chat-app.tsx` - Create: `components/chat/chat-app.test.tsx` - Create: `components/chat/chat-sidebar.tsx` - Create: `components/chat/chat-header.tsx` - Create: `components/chat/model-selector.tsx` - Create: `components/chat/chat-empty-state.tsx` - Create: `components/chat/chat-message-list.tsx` - Create: `components/chat/chat-message.tsx` - Create: `components/chat/chat-composer.tsx` - Create: `components/ui/textarea.tsx` - Modify: `components/auth/auth-gate.tsx` **Interfaces:** - Consumes: authenticated user, `authFetch`, chat types, `MODELS`, storage helpers, and `POST /api/chat` text stream. - Produces: `useChat(userId)`, `ChatApp({ user, onLogout })`, complete desktop/mobile chat UI. - [ ] **Step 1: Write failing streaming hook tests** Use a `ReadableStream` test response and pin lifecycle behavior: ```tsx it("adds the user message and streams assistant chunks", async () => { vi.mocked(authFetch).mockResolvedValue(streamResponse(["Привет", ", мир"])); const { result } = renderHook(() => useChat("demo-user")); await act(() => result.current.sendMessage("Тест")); expect(result.current.activeConversation?.messages.map((message) => message.content)).toEqual(["Тест", "Привет, мир"]); expect(result.current.activeConversation?.messages.at(-1)?.status).toBe("complete"); }); ``` Also test first-message title truncation, model changes applying to later requests, `AbortController.abort()` on stop, retry replacing the errored assistant message, `401` calling `onUnauthorized`, storage persistence, and rejection of empty text. - [ ] **Step 2: Run hook tests and verify failure** Run: `npm test -- hooks/use-chat.test.tsx` Expected: FAIL because `useChat` does not exist. - [ ] **Step 3: Implement the chat hook** Return this stable interface: ```ts type UseChatResult = { conversations: Conversation[]; activeConversation: Conversation | null; activeModelId: ModelId; isGenerating: boolean; selectConversation(id: string): void; newConversation(): void; deleteConversation(id: string): void; setModel(id: ModelId): void; sendMessage(content: string): Promise; stopGenerating(): void; retryLast(): Promise; }; ``` Use immutable state updates, `crypto.randomUUID()`, a retained `AbortController`, streaming `response.body.getReader()`, `TextDecoder`, and persistence after each meaningful state transition. Treat `401` as session loss and other failures as retryable assistant errors. - [ ] **Step 4: Write failing component interaction tests** Verify the product renders a new-chat empty state, model selection, `Enter` send, `Shift+Enter` newline, disabled empty send, stop button during streaming, copy action, retry action, sidebar conversation selection/deletion, mobile menu toggle, and logout. ```tsx await user.type(screen.getByLabelText("Сообщение"), "Составь план{enter}"); expect(sendMessage).toHaveBeenCalledWith("Составь план"); expect(screen.getByRole("button", { name: "Остановить генерацию" })).toBeInTheDocument(); ``` - [ ] **Step 5: Implement focused chat components** Use `Button`, `Input`, and `Textarea` shadcn primitives plus Lucide icons. The desktop sidebar is 272px wide; mobile uses an overlay drawer. The header contains a shadcn-style model trigger/menu and mobile menu action. The empty state says `Чем я могу помочь?` with three realistic prompt suggestions. User messages use subtle right-aligned bubbles; assistant messages use a clean full-width reading column with copy/retry controls. The composer is centered, grows up to 200px, and includes the model disclaimer. Use real buttons with accessible names, focus-visible styling, `aria-live="polite"` for incoming text, and `aria-busy` during generation. Preserve whitespace with CSS instead of an HTML renderer. - [ ] **Step 6: Connect authenticated user and logout** Render `` only in `AuthGate`'s authenticated state. Pass an `onUnauthorized` callback into `useChat` so a `401` clears the token and returns to login without rendering stale chat controls. - [ ] **Step 7: Run UI tests and commit** Run: `npm test -- hooks/use-chat.test.tsx components/chat/chat-app.test.tsx components/auth/auth-gate.test.tsx` Expected: PASS. Commit: ```bash git add hooks components/chat components/ui/textarea.tsx components/auth/auth-gate.tsx git commit -m "feat: build responsive streaming chat" ``` --- ### Task 6: Product Styling, Security Headers, and Final Verification **Files:** - Modify: `app/globals.css` - Modify: `app/layout.tsx` - Modify: `next.config.ts` - Modify: `README.md` - Create: `public/og.png` **Interfaces:** - Consumes: complete auth/chat UI. - Produces: finished responsive product, security headers, operational setup instructions, and social preview metadata. - [ ] **Step 1: Add a failing security configuration assertion** Create `next-config.test.ts` and assert the exported headers include `Content-Security-Policy`, `Referrer-Policy`, `X-Content-Type-Options`, and `X-Frame-Options` for all routes. The CSP must include `default-src 'self'`, `object-src 'none'`, `base-uri 'self'`, `frame-ancestors 'none'`, and no third-party origins. - [ ] **Step 2: Run the security test and verify failure** Run: `npm test -- next-config.test.ts` Expected: FAIL because `next.config.ts` has no headers. - [ ] **Step 3: Finish the visual system and metadata** Replace starter metadata with Russian product metadata: ```ts export const metadata: Metadata = { title: "AI Control Center", description: "Корпоративный интерфейс для работы с ИИ-моделями и агентами.", }; ``` Set ``. In `globals.css`, define the warm neutral app background, sidebar surface, readable message width, composer shadow, overlay animation, responsive breakpoints, reduced-motion behavior, and full-height `body`. Keep both light and dark token definitions coherent even though the first screen defaults to light. - [ ] **Step 4: Add static CSP and security headers** Use `next.config.ts` `headers()` rather than a nonce proxy so static rendering remains available. Allow only self-hosted scripts, fonts, and network requests; permit Next/React development requirements conditionally (`'unsafe-inline'` and development-only `'unsafe-eval'`). Add `object-src 'none'`, `base-uri 'self'`, `form-action 'self'`, `frame-ancestors 'none'`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`, and `X-Frame-Options: DENY`. - [ ] **Step 5: Generate and validate one product social card** Create exactly one landscape `public/og.png` representing the finished AI Control Center: neutral ChatGPT-like application frame, sidebar, model selector, message composer, Russian title `AI Control Center`, and no provider logos. Inspect the image once; retry only if text is unusable. Add Open Graph and X metadata only after the asset passes inspection. - [ ] **Step 6: Replace the starter README** Document `npm install`, `.env.local` variable names, the test login, `AI_GATEWAY_MOCK=true`, the exact Gateway request/stream contract, and `npm run dev`, `npm test`, `npm run lint`, `npm run build`. Explicitly warn that `JWT_SECRET`, `AUTH_PASSWORD`, and gateway keys must not be committed. - [ ] **Step 7: Run all automated verification** Run individually and fix only actual failures: ```bash npm test npm run lint npm run build ``` Expected: all commands exit `0` with no failing tests or TypeScript/build errors. - [ ] **Step 8: Run runtime API smoke checks without browser automation** Start `npm run dev` in a retained session. Send a login request, call `/api/auth/me` and `/api/chat` with the returned Bearer token, and confirm the mock chat response streams as plain text. Repeat `/api/auth/me` without the header and confirm `401`. Stop the server after verification. Browser clicking and screenshots are omitted because they were not requested; component tests cover the corresponding interactions. - [ ] **Step 9: Commit the finished product** ```bash git add app components hooks lib next.config.ts next-config.test.ts public/og.png README.md git commit -m "feat: complete corporate AI chat experience" ```