Delete the public /api/config/gemini endpoint and remove client auto-load paths so GEMINI_AI_API_KEY stays server-side only. Add a scan rate-limit class for the upcoming server-side identify route and a CI gate that blocks reintroducing config key leaks or new browser LLM URLs. Co-authored-by: Cursor <cursoragent@cursor.com>
4.9 KiB
| name | classification | success_metric | skip | status | created | |||||
|---|---|---|---|---|---|---|---|---|---|---|
| secure-scanner-gemini-key | server-only | 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. |
|
in-progress | 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/geminiand 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 sixthscanentry toLIMITER_CONFIG(5 req / 1 min, user-keyed) and exportcheckScanRateLimit(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 blockingforbidden-client-side-llm-keysjob (~30s grep gate, same shape asforbidden-cors-headers):- Fail if
apiKey:appears underpages/api/config/*. - Fail if
generativelanguage.googleapis.comorapi.openai.comappears outsidepages/api/(client-side LLM URL leakage).
- Fail if
.cursor/rules/api-routes.mdc§ Rate limiting — document the newscanclass (architect may fold into brief or defer to convoy #2; implementer should at minimum add the lib export).
Out of scope
/api/scan/identifyserver route — convoyserver-side-scan-pipeline(#2).- Deleting
lib/ai-ocr.jsbrowser 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
role-architect— ratify scan limiter values; confirm grep patterns for the CI gate; single brief decomposition.role-implementer— one brief, no fan-out.role-reviewer— post-PR audit (design + a11y skipped perskip:).
Todos
- Architect: write
.convoys/secure-scanner-gemini-key/brief-1-*.md - Implementer: delete config endpoint + client auto-load; extend rate-limit lib
- Implementer: add
forbidden-client-side-llm-keysCI job - Operator: rotate
GEMINI_AI_API_KEYin Google AI Studio + Vercel before merge - Reviewer: verify no key material in diff or CI logs
Operator action required
URGENT — rotate before merge:
- Google AI Studio — revoke the current
GEMINI_AI_API_KEYand issue a new key. The old key has been exposed to every browser session that loaded the scanner page. - Vercel — update the
GEMINI_AI_API_KEYenv var on Production + Preview to the new value. - Verify — after deploy, confirm
GET /api/config/geminireturns 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.