fix: never send an API key to the provider it wasn't entered for (v1.0.2)
All checks were successful
CI — Test & Build / Test & Build (push) Successful in 1m33s

A stored key carried no record of which provider it belonged to. Options
saves {provider, model} without the key whenever the field is blank (which
it always is after a save, since it shows the encrypted badge instead), so
switching provider left the previous provider's key attached to the new one.
Every call then failed with that provider's own "Invalid API Key" while the
UI still showed a key as configured.

- types.ts: new `keyProvider` storage field, added to CONFIG_STORAGE_KEYS
- background.ts: keyProviderMismatch() guards the chat, COPY_AS and
  stored-key LIST_MODELS paths; absent keyProvider (pre-upgrade) is allowed
- Options.tsx: stamps keyProvider on every save; drops the encrypted badge
  and requires a new key when the saved one belongs to another provider or
  is rejected; save-time guard messages are now actually rendered (they were
  gated on modelsStatus === 'error' and never drew, so Save looked dead)
- providers.ts: providerLabel(); settings hint appended to 401/403 only;
  listModels reports keyRejected and labels errors with the display name
- Anthropic: send anthropic-dangerous-direct-browser-access on the chat path

Docs: CLAUDE.md version-bump rule corrected — wxt.config.ts reads
pkg.version, so package.json is the only place to edit.

typecheck clean, 58/58 tests, build clean (281.72 kB, manifest 1.0.2).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
john kevin asprec
2026-07-23 15:50:07 +08:00
parent d5a2f4a0be
commit 6aee260533
9 changed files with 176 additions and 45 deletions

View File

@@ -3,26 +3,14 @@
## Current state
- **Outcome:** Phase 1 complete (7 workitems, 2026-03-06). Codebase reviewed 2026-07-13 (`RECOMMENDATIONS.md`). Operating-system docs + agent roster aligned to the template 2026-07-15.
- **Delivered:** working MV3 extension — selection detection (textarea/input/contenteditable), floating toolbar, background LLM proxy with OpenAI/Anthropic/Groq/OpenRouter, Options page, result modal with Replace/Copy. Build ~166 KB.
- **Verified (Phase 1):** `npm run build` clean. Note: unit tests currently exercise the storage mock, not the real handlers; e2e has `[EXTENSION_ID]` placeholders and won't pass as-is.
- **Verified (2026-07-15 finalize/build):** `npm run typecheck` clean, `npm test -- --run` 46/46 passing (actions, messaging, crypto, providers), `npm run build` clean → `.output/chrome-mv3/` (265.82 kB) ready for load-unpacked testing. Recent refactor series (crypto consolidation, provider adapter table, context-menu registry, dev-gated debug logs) all pass gates; real-page Replace verification still pending on the user's load-unpacked check.
- **Fix (2026-07-15):** OpenAI adapter now sends `max_completion_tokens` instead of the legacy `max_tokens` (newer OpenAI models reject it) and omits `temperature` for reasoning models (`o*`/`gpt-5*`, which only accept the default). Groq/OpenRouter unchanged (they still expect `max_tokens`). Pinned tests updated + new reasoning-model test; typecheck/47 tests/build all green; rebuilt `.output/chrome-mv3/`.
- **Feature (2026-07-15):** Options form reordered to Provider → API Key → Model (model list is fetched with the key). New `prompt` action ("Make Prompt", prompt-engineer): added to `ACTIONS`/labels (context menus follow automatically), prompt-engineer system prompt in `getSystemPrompt` with a prompt-directed style modifier, toolbar button (🪄 Prompt) in content.ts, popup button. Tests updated (registry count now derived; new getSystemPrompt cases); typecheck/48 tests/build green.
- **Feature (2026-07-15, Prompt Builder):** dedicated popup section for the `prompt` action with its own parameters — Prompt Style (Auto/Instructional/Role-play/Step-by-step/Few-shot/Structured), Persona (presets + Custom free text + None), Output Format (Auto/Plain/Markdown/Bulleted/Numbered/JSON/Table). Constants in `src/lib/actions.ts`, `PromptParams` added to `AnalyzePayload` (both message shapes still supported), composed into the system prompt by `promptParamModifiers` in providers.ts (prompt action only; 'Auto' = no-op). Params persist in `chrome.storage.local`. Toolbar 🪄 Prompt keeps Auto defaults. typecheck/49 tests/build green.
- **Feature (2026-07-15, popup tabs + model picker):** popup restructured into two tabs — "✍ Writing" (style + fix/rephrase/shorten/expand) and "🪄 Prompt Builder" (prompt params + Make Prompt). Prompt Builder gained a Model picker: fetched via `LIST_MODELS` on first tab open, '' = configured default; selection sent as new optional `model` override on `AnalyzePayload` (both message shapes), applied in background's `handleAnalyzeText`. Tab + model persist in storage. typecheck/49 tests/build green.
- **Tweak (2026-07-15):** model picker's "Default (model)" label → plain "Default" (badge already shows the model). Toolbar/context-menu `prompt` requests now inherit the saved Prompt Builder settings: background's `handleAnalyzeText` loads `promptStyle/promptPersona/customPersona/promptFormat/promptModel` from storage when the payload has no `promptParams` (popup still sends explicit ones); persona resolution shared via `resolvePromptPersona` in actions.ts. typecheck/49 tests/build green.
- **Feature (2026-07-15, in-page Prompt Builder dialog):** toolbar 🪄 Prompt and context-menu "Make Prompt" now open an on-page dialog (content.ts `showPromptBuilderDialog`) with the popup's parameters (Style/Persona+custom/Format/Model); selections persist to the shared storage keys, then the request runs without explicit params (background applies saved). Context menu: prompt is now a single item (no style children — registry excludes it; tests updated). `LIST_MODELS` resolves provider from storage when omitted. Toolbar re-clamps position using its real width so the last buttons stay on-screen. typecheck/49 tests/build green. Needs a real-page check (dialog + replace are DOM-timing-sensitive).
- **Fix (2026-07-15, prompt UX chain):** Make Prompt no longer closes the dialog silently — the dialog becomes a "⟳ Building your prompt…" spinner and `runAction` closes it (`closePromptBuilder`) at every completion path (success, error, invalidated-context). Prompt result modal is prompt-specific: "🪄 Engineered Prompt" title, and the writing-style selector row is replaced by "✎ Edit Parameters" (reopens the builder dialog) + Regenerate (re-runs with saved builder params). typecheck/49 tests/build green.
- **Security/perf pass (2026-07-15, goal-driven audit):** reviewed key safety, user-text privacy, and content-script performance; fixed:
- `src/lib/crypto.ts` — new `migratePlaintextApiKey()`: background auto-encrypts a legacy plaintext `apiKey` on worker start and removes the plaintext (write-and-await key material before delete; verify-decrypt before dropping plaintext when an encrypted key already exists; re-check for a concurrent Options save before writing). Read-path plaintext fallback in `resolveApiKey` retained per invariant.
- `entrypoints/background.ts` — calls the migration on startup; `sender.id !== chrome.runtime.id` guard on `onMessage` (defense-in-depth; internal senders unaffected).
- `entrypoints/content.ts` — fixed unbounded document-listener leak: `showToolbar` added `click`/`scroll` listeners per selection and never removed them; now unregistered in `hideToolbar` (also closes an orphaned More-menu). Error toast gained `data-lexai`; mouseup threshold now uses `MIN_SELECTION_LENGTH` (was hardcoded `<= 10`, which ate exactly-10-char selections); same sender guard on its listener.
- `src/lib/providers.ts``callProvider` guards `res.json()` so non-JSON gateway errors (HTML 502) surface as `"<Provider> error: HTTP <status>"` instead of a raw SyntaxError.
- Verified: typecheck clean, 54/54 tests (6 new: 4 migration, 1 non-JSON error, plus existing), build clean (280.3 kB). `security-auditor` reviewed the key-path diff: PASS; its two P3 hardening notes (await encKey persistence, concurrent-save re-check) were implemented and re-gated. User-text privacy audited clean: no persistence of analyzed text, debug logs dev-gated, key/text travel only to the chosen provider. Real-page load-unpacked check of toolbar/replace still recommended (DOM-timing paths untouched except listener cleanup).
- **CI fix + workflow hardening (2026-07-15):** CI typecheck failed on push (`content.ts(10,31) TS2339: Property 'env' does not exist on type 'ImportMeta'`) because `.wxt/types/` (which types `import.meta.env`) is generated by `wxt prepare`, which CI never ran — locally it existed as a side effect of `wxt build`/`dev`. Reproduced locally by deleting `.wxt/`. Fixes: `postinstall: wxt prepare` in package.json (root fix — every fresh install regenerates types); explicit "Prepare WXT types" step in both workflows (survives a future `--ignore-scripts`); `deploy-chrome.yml` now runs **typecheck** before tests (release previously gated less than CI); fixed `head -1``sed '$d'` body extraction in both curl-response checks (body is all-but-last-line, not first line); added `timeout-minutes` (15 CI / 20 deploy). Verified: fresh install regenerates `.wxt`, then typecheck + 54/54 tests + build all green. CLAUDE.md Commands section documents the gotcha.
- **Changed paths (this alignment):** added `docs/` (brief, architecture, decisions, tasks, evals, lessons, handoff, self-model, attacksurface), ported `.claude/agents/*` roster + `.claude/skills/*`, kept `lexai-extension-dev`, updated `.claude/AGENTS.md`. `CLAUDE.md` restructured to the operating-system format (all original LexAI rules preserved).
- **Delivered:** working MV3 extension — selection detection (textarea/input/contenteditable), floating toolbar, background LLM proxy with OpenAI/Anthropic/Groq/OpenRouter, Options page, result modal with Replace/Copy.
- **Shipped 2026-07-15 (consolidated):** refactor series (crypto consolidation, provider adapter table, context-menu registry, dev-gated debug logs); OpenAI `max_completion_tokens` + no-temperature for reasoning models; live model listing in Options (Provider → API Key → Model); new `prompt` action + Prompt Builder (popup tabs, in-page dialog, persona/style/format/model params shared via storage); security/perf pass (plaintext-key migration, `sender.id` guard, content-script listener leak fix, non-JSON error guard); CI fix — `postinstall: wxt prepare` (CI never ran it, so `.wxt/types` was missing and typecheck failed on `import.meta.env`) plus workflow hardening. All gated: typecheck + tests + build.
- **Fix (2026-07-23, Anthropic CORS):** `src/lib/providers.ts:172` — the Anthropic chat spec now sends `anthropic-dangerous-direct-browser-access: 'true'` (the model-list path at `:276` already did). Verified present in `.output/chrome-mv3/background.js`, which is the only bundle that reaches `api.anthropic.com`. **Unresolved for the user:** the CORS error still appears in their browser, which means the running extension is older than this build (a stale service worker, or a second copy installed from the pre-fix `.output/lexai-1.0.1-chrome.zip` dated 7/15). Next diagnostic: service-worker inspector → Network → `messages` → check Request Headers.
- **Fix (2026-07-23, Groq key rejected):** the Groq spec was correct; the Options flow was not. (1) `handleSave` set `modelsError` but the render gated it on `modelsStatus === 'error'`, so both save-time guards were invisible and Save silently no-opped — now rendered whenever set (amber for guidance, red for load errors). (2) `handleProviderChange` auto-listed models with the *stored* key after a provider switch, so Groq rejected the previous provider's key ("Invalid API Key") before any Groq key was entered — now tracked via `savedKeyProvider` ref; it prompts for the new key instead of guessing. (3) `listModels` errors now use `spec.label` (`Groq error: …`, matching the chat path) instead of the raw id (`groq error: …`).
- **Root cause + fix (2026-07-23, Groq "Invalid API Key"):** a stored key had no record of the provider it was entered for. `Options.handleSave` writes `{provider, model}` **without** the key when the field is blank and one is stored, so switching to Groq and saving left the OpenAI key attached to Groq — every call, and every stored-key model list, sent it and got that provider's own rejection while the field still showed 🔒. Fix: new `keyProvider` storage field (`types.ts`, in `CONFIG_STORAGE_KEYS`) written on every save; `keyProviderMismatch()` in background.ts blocks the send on the chat, COPY_AS, and stored-key LIST_MODELS paths with an actionable message (absent `keyProvider` = pre-upgrade key, allowed); Options drops the 🔒 badge and demands a new key when the saved one belongs to another provider or comes back rejected (`keyRejected` flag from `listModels` on 401/403); `callProvider` appends "open LexAI Settings and re-enter your API key" to 401/403 only.
- **Verified (2026-07-23):** `npm run typecheck` clean, `npm test -- --run` 58/58 (new: Groq bearer auth, labelled errors, the 401 hint, `keyRejected`, `providerLabel`), `npm run build` clean → `.output/chrome-mv3/` 281.72 kB. Options-page behavior is **not** covered by unit tests — a load-unpacked check of the Groq re-entry flow is still pending.
- **Open risks (ranked):**
1. `<all_urls>` host permission — privacy surface + CWS review blocker (TASKS #1).
2. Key "encryption" is obfuscation (`encKey` co-located) — TASKS #2.
3. Tests don't cover real code paths (TASKS #8) or DOM replace (TASKS #10).
- **Next smallest action:** run the quick wins in order — T-03 (gate debug logs), then T-06/T-09/T-15/T-16 — each is small and independent. Do T-01/T-02 before any Chrome Web Store push.
3. Tests don't cover real code paths (TASKS #8), DOM replace (TASKS #10), or any Options/Popup React flow.
- **Next smallest action:** reload the unpacked extension, then in Options **re-enter the Groq key** (this stamps `keyProvider` and replaces the mis-attached key) → ↻ Load → select model → Save, and confirm a real-page action. Then the quick wins: T-03, T-06/T-09/T-15/T-16. Do T-01/T-02 before any Chrome Web Store push.