Skip to content

AI: OpenAI proxy via free Cloudflare Worker (Firebase-token auth) — replaces Blaze-only callable - #4

Open
notAidven wants to merge 2 commits into
feature/casino-2rooms-preflopfrom
feature/openai-proxy
Open

AI: OpenAI proxy via free Cloudflare Worker (Firebase-token auth) — replaces Blaze-only callable#4
notAidven wants to merge 2 commits into
feature/casino-2rooms-preflopfrom
feature/openai-proxy

Conversation

@notAidven

@notAidven notAidven commented Jun 25, 2026

Copy link
Copy Markdown
Owner

Summary

Re-hosts the secure OpenAI proxy on a free-tier Cloudflare Worker instead of a Firebase Cloud Function, so the "Suited" app can use OpenAI without enabling the Blaze plan (this project is on the free Spark plan; Cloud Functions require Blaze). The OpenAI key stays server-side and the client keeps its soft-fail → rule-based fallback.

This replaces the previous approach in this PR (the aiChat Firebase callable). It stacks on #3 (feature/casino-2rooms-preflop) because the AI layer (web/src/lib/ai/*) and Room 2 LLM opponents live on the casino stack, not main.

Default behavior is unchanged: provider selection still defaults to gemini (Firebase AI Logic) → rule-based. The proxy is strictly opt-in.

Worker design (worker/)

A small TypeScript Worker (name = suited-ai-proxy) with zero runtime dependencies (only wrangler / @cloudflare/workers-types / typescript as devDeps).

  • POST /chat — accepts { model?, messages, temperature?, max_tokens?, json? }, validates it (roles/content, array size ≤ 50, content ≤ 24k chars, temperature 0–2, positive-int max_tokens capped at 4096), then calls OpenAI Chat Completions (https://api.openai.com/v1/chat/completions). Default model gpt-4o-mini; json: true sets response_format: { type: 'json_object' }. Returns { text, model, finishReason }.
  • GET / (or /health) — unauthenticated liveness probe → { "status": "ok" } (handy to confirm a deploy).
  • Secret — the key is read from the Worker secret OPENAI_API_KEY (env.OPENAI_API_KEY), set via wrangler secret put. It is never in any file, committed, logged, or returned. Errors are sanitized to clean messages.

Auth gating — Firebase ID token verify (Web Crypto, no deps)

Every /chat call must send Authorization: Bearer <Firebase ID token>. The Worker verifies it itself with Web Crypto (chosen over a library to keep deps minimal and avoid needing a KV namespace on the free tier):

  1. Fetches Google's public x509 certs from https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com and caches them per the response's Cache-Control: max-age (in-isolate).
  2. Parses the cert DER to extract its SubjectPublicKeyInfo and verifies the JWT's RS256 signature over header.payload.
  3. Checks claims: aud === "brilliant-alpha-clone-54be9", iss === "https://securetoken.google.com/brilliant-alpha-clone-54be9", exp not expired (+ light iat/auth_time/sub checks).

Missing/invalid/expired token → 401.

CORS

Allows https://brilliant-alpha-clone-54be9.web.app, https://brilliant-alpha-clone-54be9.firebaseapp.com, and http://localhost:5173/5174/5175; handles OPTIONS preflight and echoes the allowed Origin with proper Access-Control-* headers (Vary: Origin).

Client wiring (web/)

  • web/src/lib/ai/providers/openai-proxy.ts rewritten to call the Worker via fetch (POST JSON, Authorization: Bearer <token from auth.currentUser.getIdToken()>) instead of httpsCallable. The Worker URL comes from VITE_AI_PROXY_URL (the client appends /chat; base URL or full .../chat both work).
    • generateText / generateJSON / streamText mapping preserved (streaming = one non-streamed call emitted once).
    • Soft-fail-to-null preserved: unset VITE_AI_PROXY_URL, no sign-in, network/timeout, or non-2xx → null, so Room 2's rule-based fallback (and the coach/table-talk) still work and nothing crashes.
    • Still opt-in via VITE_LLM_PROVIDER=openai-proxy; default (gemini → rule-based) unchanged and never auto-selected.
  • web/.env.example documents VITE_AI_PROXY_URL + the deploy/opt-in flow.

Removed (no more Cloud Functions)

  • Deleted the functions/ directory (the aiChat callable project).
  • Removed the functions block from firebase.json (hosting / firestore / auth config untouched).
  • Deleted web/src/lib/firebaseFunctions.ts (only the old provider imported it; confirmed unused after the rewrite).

Verification

From web/:

  • npm run buildexit 0
  • npx vitest run409 passed (5 files)
  • node_modules/.bin/tsc -p tsconfig.app.json --noEmitclean
  • node scripts/mvp-logic-check.mjsall PASS
  • npx eslint on the changed provider — clean

From worker/:

  • npm install — exit 0 (only devDeps)
  • npx tsc --noEmitclean
  • npx wrangler deploy --dry-run --outdir distbuilds OK (~13 KiB, no deploy)
  • Auth crypto path proved with an offline round-trip: extracting the SPKI from v1 & v3 self-signed certs + verifying a good RS256 signature (and rejecting a tampered one), and importing all 5 real Google securetoken certs via Web Crypto.

Not exercised (needs the maintainer's key + accounts): real OpenAI completions and a live deployed Worker round-trip.

Deploy steps (maintainer — all free)

  1. cd worker && npm install
  2. npx wrangler login (free Cloudflare account)
  3. npx wrangler secret put OPENAI_API_KEY (paste the sk-... key)
  4. npx wrangler deploy → note the Worker URL (https://suited-ai-proxy.<subdomain>.workers.dev)
  5. In web/.env.local: VITE_AI_PROXY_URL=<that URL> and VITE_LLM_PROVIDER=openai-proxy
  6. cd ../web && npm run build && firebase deploy --only hosting

Notes

Made with Cursor

notAidven and others added 2 commits June 25, 2026 18:33
…provider

Add an OpenAI option that runs through a Firebase callable so the browser
never holds the API key. Default behavior is unchanged (gemini, then
rule-based); the proxy is strictly opt-in.

functions/ (new 2nd-gen Cloud Functions, TypeScript):
- `aiChat` callable: REQUIRES auth (rejects when request.auth is missing),
  reads the key from the v2 secret OPENAI_API_KEY (bound via `secrets`),
  calls OpenAI Chat Completions via the official `openai` package, supports
  a JSON-output mode (response_format json_object), and validates input.
  Errors return a clean message and never leak the key/request.
- Adds the "functions" codebase to firebase.json (build predeploy).

web/ client wiring:
- New `openai-proxy` provider (providers/openai-proxy.ts) conforming to the
  existing LLMProvider interface; calls aiChat via httpsCallable. generateText
  (and thus aiClient.generateJSON) route through it; streamText falls back to
  a single non-streamed call. All failures soft-fail to null.
- New firebaseFunctions.ts lazy Functions accessor (keeps firebase config
  untouched). Registered in providers/index.ts and selectable via
  VITE_LLM_PROVIDER=openai-proxy (opt-in; gemini stays the default).
- Room 2 LLM opponents + AI coach/table-talk are provider-agnostic, so they
  use the proxy when selected while preserving the existing fallback chain
  (rule-based on AI off / proxy error). Room 1's rule-based coach untouched.

SECURITY: the OPENAI_API_KEY secret is set and the function deployed
SEPARATELY by the maintainer; no key is hardcoded, logged, or committed.

Co-authored-by: Cursor <cursoragent@cursor.com>
… callable)

Cloud Functions require the Blaze plan; this project is on the free Spark plan.
Replace the aiChat Firebase callable with a free-tier Cloudflare Worker that keeps
the OpenAI key server-side, so AI works without enabling billing.

- worker/: TypeScript Worker (POST /chat) validates input, calls OpenAI Chat
  Completions (default gpt-4o-mini, optional json_object), returns
  { text, model, finishReason }. OPENAI_API_KEY is a Worker secret (never committed).
- Auth gating: requires Authorization: Bearer <Firebase ID token>; verifies RS256
  against Google's securetoken x509 certs with Web Crypto (zero runtime deps),
  checks aud/iss/exp, caches certs per Cache-Control. 401 on missing/invalid.
- CORS: allows the hosting domains + http://localhost:5173/5174/5175; OPTIONS preflight.
- Client: providers/openai-proxy.ts now fetches the Worker URL (VITE_AI_PROXY_URL)
  with the user's ID token instead of httpsCallable; same soft-fail-to-null fallback;
  still opt-in via VITE_LLM_PROVIDER=openai-proxy (default gemini->rule-based unchanged).
- Remove functions/ and the firebase.json functions block; drop now-unused
  web/src/lib/firebaseFunctions.ts. Hosting/Firestore/Auth config unchanged.

Deploy (free): wrangler login -> wrangler secret put OPENAI_API_KEY -> wrangler deploy
-> set VITE_AI_PROXY_URL -> rebuild web & redeploy hosting (see worker/README.md).

Co-authored-by: Cursor <cursoragent@cursor.com>
@notAidven notAidven changed the title AI: secure server-side OpenAI proxy (aiChat callable) + openai-proxy provider AI: OpenAI proxy via free Cloudflare Worker (Firebase-token auth) — replaces Blaze-only callable Jun 26, 2026
notAidven added a commit that referenced this pull request Jun 26, 2026
 Cloudflare proxy) into integration

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	web/src/pages/ProfilePage.tsx
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