Skip to content

fix(cors): allow dashboard origins for public audit reads#6

Merged
hizrianraz merged 1 commit into
mainfrom
fix/cors-dashboard-origins
May 16, 2026
Merged

fix(cors): allow dashboard origins for public audit reads#6
hizrianraz merged 1 commit into
mainfrom
fix/cors-dashboard-origins

Conversation

@hizrianraz
Copy link
Copy Markdown
Contributor

@hizrianraz hizrianraz commented May 16, 2026

Summary

  • Add CORSMiddleware to FastAPI app, allowing browser reads from app.ainfera.ai, *.vercel.app previews, marketing site, and local dev.
  • 4 unit tests covering allowed origins, Vercel-preview regex, OPTIONS preflight on /v1/audit/public, and unlisted-origin rejection.

Why

The dashboard at app.ainfera.ai calls api.ainfera.ai/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.

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 green
  • pytest tests/unit/ — 40/40 green (no regressions)
  • ruff check clean
  • mypy --strict ainfera_api/main.py clean
  • After deploy: curl -sI -H "Origin: https://app.ainfera.ai" https://api.ainfera.ai/v1/audit/public | grep access-control-allow-origin should return the dashboard origin
  • After deploy: app.ainfera.ai/audit should render 15 events for hizrianraz

🤖 Generated with Claude Code


Note

Medium Risk
Adds global CORS policy (including allow_credentials=True and 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 CORSMiddleware to the FastAPI app, allowing requests from the dashboard, marketing domains, localhost dev, and *.vercel.app preview origins (via regex), and supporting preflight OPTIONS.

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.

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>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

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.

Comment thread ainfera_api/main.py
"http://localhost:3001",
],
allow_origin_regex=r"https://[a-z0-9-]+\.vercel\.app",
allow_credentials=True,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0d300f6. Configure here.

@hizrianraz hizrianraz merged commit 90a3adf into main May 16, 2026
4 checks passed
@hizrianraz hizrianraz deleted the fix/cors-dashboard-origins branch May 16, 2026 08:11
hizrianraz added a commit that referenced this pull request May 16, 2026
…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>
hizrianraz added a commit that referenced this pull request May 18, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant