deckhearth/.convoys/secure-scanner-gemini-key.md

125 lines
4.9 KiB
Markdown
Raw Normal View History

---
name: secure-scanner-gemini-key
classification: server-only
success_metric: |
GEMINI_AI_API_KEY rotated; no endpoint returns the key to a browser;
lib/rate-limit.js exposes a sixth scan class (5 req / 1 min, user-keyed);
forbidden-client-side-llm-keys CI gate green.
skip:
- ia
- ux
- visual
- a11y
- design
status: in-progress
created: 2026-05-27
---
# Convoy: secure-scanner-gemini-key
Stop leaking the Gemini API key to browsers and add a scan-specific rate
limiter before the server-side scan pipeline lands in convoy #2.
## Why
The scanner audit found that `pages/api/config/gemini.js` returns
`process.env.GEMINI_AI_API_KEY` to any authenticated caller, and
`components/CameraScanner.js` auto-fetches that endpoint on mount (lines
5367). A logged-in user — or anyone who obtains a session token — can
read the production LLM key from DevTools and call Gemini directly,
bypassing app rate limits and billing controls. This is a **live secret
exposure** that must be closed before any further scanner work ships.
## Scope
### In scope
- **Delete** `pages/api/config/gemini.js` — remove the key-returning
endpoint entirely.
- **`components/CameraScanner.js`** — remove the auto-load block at lines
5367 that fetches `/api/config/gemini` and stores the key client-side.
The scanner will break temporarily until convoy #2 lands; that is
acceptable for the security-only window.
- **`lib/rate-limit.js`** — add a sixth `scan` entry to `LIMITER_CONFIG`
(5 req / 1 min, user-keyed) and export `checkScanRateLimit(req,
userId)` following the Decision-2 hybrid named-limiter pattern from
`.convoys/add-rate-limiting.md`. Redis prefix: `deckhearth:scan`.
- **`.github/workflows/ci.yml`** — add a new blocking
`forbidden-client-side-llm-keys` job (~30s grep gate, same shape as
`forbidden-cors-headers`):
- Fail if `apiKey:` appears under `pages/api/config/*`.
- Fail if `generativelanguage.googleapis.com` or `api.openai.com`
appears outside `pages/api/` (client-side LLM URL leakage).
- **`.cursor/rules/api-routes.mdc`** § Rate limiting — document the new
`scan` class (architect may fold into brief or defer to convoy #2;
implementer should at minimum add the lib export).
### Out of scope
- **`/api/scan/identify`** server route — convoy `server-side-scan-pipeline` (#2).
- **Deleting `lib/ai-ocr.js` browser classes** (`GeminiVisionOCR`,
`AICardOCR`, etc.) — deferred to #2 to avoid breaking the live scanner
during this security-only window (the client still needs those classes
until the server pipeline replaces them).
- **Rotating the key in source control** — the key lives in Vercel env
only; rotation is operator action (see below).
## Roles invoked
1. `role-architect` — ratify scan limiter values; confirm grep patterns
for the CI gate; single brief decomposition.
2. `role-implementer` — one brief, no fan-out.
3. `role-reviewer` — post-PR audit (design + a11y skipped per `skip:`).
## Todos
- [ ] Architect: write `.convoys/secure-scanner-gemini-key/brief-1-*.md`
- [x] Implementer: delete config endpoint + client auto-load; extend rate-limit lib
- [x] Implementer: add `forbidden-client-side-llm-keys` CI job
- [x] Operator: rotate `GEMINI_AI_API_KEY` in Google AI Studio + Vercel **before merge**
- [ ] Reviewer: verify no key material in diff or CI logs
## Operator action required
**URGENT — rotate before merge:**
1. **Google AI Studio** — revoke the current `GEMINI_AI_API_KEY` and
issue a new key. The old key has been exposed to every browser session
that loaded the scanner page.
2. **Vercel** — update the `GEMINI_AI_API_KEY` env var on Production +
Preview to the new value.
3. **Verify** — after deploy, confirm `GET /api/config/gemini` returns
404 and no network tab in the scanner shows key material.
Do **not** merge this PR until rotation is complete. The CI gate prevents
re-introduction of the leak pattern but does not invalidate a key that
was already exfiltrated.
## Multitask dispatch
Single brief — **no implementer fan-out**.
Post-PR audit fan-out:
```
/multitask role-reviewer
```
Group id: `audit-secure-scanner-gemini-key-<pr>` (design-system + a11y
auditors skipped per `skip:` flags).
**Cross-convoy parallelism (after #1 merges):** open three worktrees and
`/multitask role-implementer` on **#2 Brief 1 + #5 Brief 1 + #6 (sole
brief)** — disjoint file sets (migrations / UI strings / scanner polish).
## CI impact
| Workflow / job | Behavior |
| --- | --- |
| `ci.yml``forbidden-client-side-llm-keys` | **New blocking job.** ~30s grep; no `npm ci`. |
| `ci.yml` → lint, vitest, forbidden-endpoints, forbidden-cors-headers | Unchanged; must stay green. |
| `preview-smoke.yml` | Fires (no `paths:` filter). Scanner page may error until #2; smoke spec does not exercise scanner. |
| `visual-diff.yml` | **Does not fire** — API-only + `lib/` + CI YAML; `!pages/api/**` exclusion applies. |
**Preserve `pipeline: skip smoke`** PR-body directive for iterative pushes.