---
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
53–67). 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
  53–67 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.
