fix(cors): allow dashboard origins for public audit reads#6
Conversation
The dashboard at app.ainfera.ai (and Vercel preview deployments) fetches /v1/audit/public directly from the browser to render the live audit chain, Overview stat cards, and per-agent feeds. The api was returning no Access-Control-Allow-Origin header, so browsers silently dropped the response and every audit-dependent surface rendered empty. Add CORSMiddleware allowing: - https://app.ainfera.ai (production dashboard) - https://{ainfera,www.ainfera}.ai (marketing) - https://*.vercel.app (preview deploys) - http://localhost:3000-3001 (local dev) Tests cover allowed origins, preflight on /v1/audit/public, and ensure unlisted origins do not receive ACAO. Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issue.
Reviewed by Cursor Bugbot for commit 0d300f6. Configure here.
| "http://localhost:3001", | ||
| ], | ||
| allow_origin_regex=r"https://[a-z0-9-]+\.vercel\.app", | ||
| allow_credentials=True, |
There was a problem hiding this comment.
Overly broad Vercel regex allows any origin with credentials
High Severity
The allow_origin_regex pattern r"https://[a-z0-9-]+\.vercel\.app" matches any *.vercel.app subdomain, not just Ainfera's preview deployments. Since Vercel is a free public hosting platform, any attacker can deploy a malicious site there (e.g. https://evil.vercel.app). Combined with allow_credentials=True, the browser will send session cookies to the API and let the attacker's page read the authenticated response — a classic CORS misconfiguration leading to data exfiltration. The regex needs to be scoped to Ainfera's project names only.
Reviewed by Cursor Bugbot for commit 0d300f6. Configure here.
…nt test (#13) The 5 AAMC v1.0 canonical voters (claude-opus-4-7, gpt-5-5, gemini-3-1-pro, grok-4, mistral-large-3) are Ainfera's exchange-layer naming. Providers churn their own model identifiers (OpenAI's "gpt-5.5-pro" is served via /v1/responses not /v1/chat/completions; Mistral re-points "mistral-large-latest" regularly; Gemini uses dotted preview names). The adapter layer needs a translation firewall so the AAMC lock can hold without depending on provider naming stability. This change: - ainfera_api/adapters/upstream_aliases.py — versioned per-provider maps with {current, history, deprecation_date} shape. Only canonical→upstream mappings that DIFFER are listed; identical names pass through (claude-opus-4-7, grok-4). Verified 2026-05-16 via direct trial inferences against each provider: * OpenAI gpt-5-5 → gpt-5.5-pro (responses-tier; works via openai.py's existing _RESPONSES_ONLY_MODELS dispatch which now operates on the translated upstream name) * Gemini gemini-3-1-pro → gemini-3.1-pro-preview (v1beta endpoint) * Mistral mistral-large-3 → mistral-large-2512 (dated, not "latest" alias — Mistral shifts that pointer) - openai.py / gemini.py / mistral.py — call resolve_upstream() at the top of chat() before building the body. OpenAI's responses-dispatch check moved to operate on the translated upstream name. - tests/integration/test_aamc_invariants.py — three assertions: 1. test_aamc_5_canonical_voters_always_active enforces Memory #6 lock at CI time. Any PR that ships a state where an AAMC voter is inactive in the migrated+seeded local DB fails here with a memory-citing message. 2. test_aamc_upstream_aliases_resolve_to_strings: cheap structural check. 3. test_aamc_upstream_translation_round_trip: live provider smoke gated on RUN_PROVIDER_SMOKE=1. - scripts/seed_dev.py — adds gpt-5-5 entry. The seed had a gap; the new CI test caught it. - scripts/fix_aamc_canon.py — Group B schema-flip script. Bundled BEGIN/RETURNING/verify/COMMIT pattern. Drops slug_format_lock CHECK (re-adds NOT VALID at end) so legacy-row UPDATEs don't trip the regex re-validation. Default dry-run; --commit gate. - scripts/rollback_aamc_canon.py — bail-out, restores pre-fix state. - scripts/rotate_tulkas_sacrificial.py — Tulkas sacrificial-key rotation (already committed live 2026-05-16; lives here for future rotations). - scripts/probe_*.py — read-only diagnostics. - pyproject.toml — per-file ruff ignores for scripts/ (long log lines OK, uppercase local-scope constants in main() intentional). After this PR + Group B (running fix_aamc_canon.py --commit against prod with founder eyes-on RETURNING), the E2E v3 check should reach 47/47 green. Co-authored-by: Claude <noreply@anthropic.com>
…llback (#44) Ainfera `slug` is the canonical-for-us identifier (e.g., `claude-opus-4-7`). The provider's API expects its own canonical name (e.g., Anthropic's `claude-opus-4-20251110`). Currently routing.dispatch_inference passes `slug` directly to upstream, which works for AAMC 5 (slug == upstream name) but blocks the 43 deactivated non-AAMC slugs whose names diverge. ## Schema (migration 0016) - `models.provider_model_name TEXT NULL` - Forward-compatible add-column (no backfill) ## Routing fallback `upstream_model = model.provider_model_name or model_slug` - NULL = adapter receives Ainfera slug as-is (current AAMC 5 behavior preserved) - Populated = adapter receives the provider's canonical name ## Audit chain integrity Per "no stealth substitution" rule, audit events still record `model_slug` (Ainfera canonical). This change only affects what we send upstream. ## Deferred to follow-up - Piece #2: fix 4 provider base_urls (groq/fireworks/deepinfra/novita) — requires prod DB UPDATE against `providers` table - Piece #3: backfill provider_model_name for the 43 deactivated rows — requires per-provider /v1/models research + sacrificial test key access + T9 re-fanout Both pieces are Discipline #6 corollary territory (prod DB writes); file as separate sub-tickets for explicit founder auth. Co-authored-by: Claude <noreply@anthropic.com>


Summary
CORSMiddlewareto FastAPI app, allowing browser reads fromapp.ainfera.ai,*.vercel.apppreviews, marketing site, and local dev./v1/audit/public, and unlisted-origin rejection.Why
The dashboard at
app.ainfera.aicallsapi.ainfera.ai/v1/audit/publicdirectly from the browser to render the live audit chain, Overview stat cards, and per-agent feeds. The api was returning noAccess-Control-Allow-Originheader, so browsers silently dropped the response and every audit-dependent surface rendered empty.The dashboard-side fixes in web PR (Phase 1 of D7) cleaned up shared layer-mapping bugs that surfaced once we instrumented this, but the actual blocker was always missing CORS.
Test plan
pytest tests/unit/test_cors.py— 4/4 greenpytest tests/unit/— 40/40 green (no regressions)ruff checkcleanmypy --strict ainfera_api/main.pycleancurl -sI -H "Origin: https://app.ainfera.ai" https://api.ainfera.ai/v1/audit/public | grep access-control-allow-originshould return the dashboard origin🤖 Generated with Claude Code
Note
Medium Risk
Adds global CORS policy (including
allow_credentials=Trueand a permissive header allowlist), so misconfiguration could unintentionally broaden cross-origin access if the origin list/regex is expanded or matched unexpectedly.Overview
Enables browser-based reads against the API by adding
CORSMiddlewareto the FastAPI app, allowing requests from the dashboard, marketing domains, localhost dev, and*.vercel.apppreview origins (via regex), and supporting preflightOPTIONS.Adds unit tests to verify allowed origins receive
Access-Control-Allow-Origin, preflight works for/v1/audit/public, and unlisted origins do not get CORS headers.Reviewed by Cursor Bugbot for commit 0d300f6. Bugbot is set up for automated code reviews on this repo. Configure here.