Skip to content

Keep the OpenAI API key out of the browser bundle - #66

Merged
Jack Williams (jack-williams) merged 6 commits into
microsoft:mainfrom
jack-williams:fix/openai-key-server-proxy
Sep 3, 2026
Merged

Keep the OpenAI API key out of the browser bundle#66
Jack Williams (jack-williams) merged 6 commits into
microsoft:mainfrom
jack-williams:fix/openai-key-server-proxy

Conversation

@jack-williams

Copy link
Copy Markdown
Contributor

Problem

VITE_OPENAI_API_KEY was read in the browser and passed to the OpenAI/Azure SDK with dangerouslyAllowBrowser: true. Vite inlines every VITE_-prefixed variable into the shipped JavaScript, so the key was extractable from the static assets by any page visitor — an anonymous credential disclosure with cost-abuse impact.

Approach

A credential-injecting reverse proxy, hosted inside the Vite dev/preview server as a plugin (packages/promptions-openai-proxy).

  • The plugin reads OPENAI_API_KEY without the VITE_ prefix, which makes inlining structurally impossible rather than merely unused.
  • The browser talks to a same-origin path (/api/openai, no hardcoded host) using a literal placeholder credential; the server swaps in the real key.
  • Request path and query are forwarded verbatim, so both OpenAI (/v1/...) and Azure (/openai/deployments/<name>/...?api-version=...) URL shapes work unchanged.
  • Auth style is inferred from OPENAI_BASE_URL and can be overridden with OPENAI_API_STYLE: Authorization: Bearer for OpenAI, the raw api-key header for Azure.
  • Only accept, content-type and openai-beta are forwarded upstream; cookies and the client's placeholder credential are dropped. Responses stream unbuffered so SSE token streaming still works, and a browser disconnect aborts the upstream request.

The dev server fails to start if VITE_OPENAI_API_KEY is still set, and warns about any other credential-shaped VITE_ variable. This matters because Vite serves the entire import.meta.env object to the browser in dev, so a key left over from the previous setup stays exposed even after the code stops reading it — deleting the code alone does not fix an existing checkout.

Also included

  • gpt-image-1 replaces dall-e-3/dall-e-2, which no longer exist on current API keys; the images API also now rejects response_format. Size/quality unions in types.ts were corrected to match.
  • OPENAI_IMAGE_MODEL — on Azure the SDK turns the request's model into a deployment name, so a hardcoded gpt-image-1 404s unless the deployment happens to share the model id.
  • The unconfigured-proxy 500 sends x-should-retry: false, so a missing key produces one request instead of three plus backoff.

Scope

This is a reference implementation. The proxy is active for vite dev and vite preview only, and it is an unauthenticated pass-through — anyone who can reach the dev server can spend the key. A static deployment of dist/ has no server and needs an equivalent proxy (for example a serverless function holding the key) in front of it. Both caveats are documented in the package README.

Verification

  • Plugin config/unit tests: 17/17 (startup failure, warning text, no secret echoed in messages, define contents, retry header)
  • Forwarding against a fake upstream: OpenAI style 14/14, Azure style 15/15 — asserts URL shape, auth header, placeholder replacement, cookie dropping and body integrity for both providers
  • No-key UI paths (Playwright): image 12/12, chat 11/11 — graceful error, app stays interactive, no direct browser-to-provider calls
  • Real-key end-to-end (since-revoked key): streaming chat and a real 1024x1024 generation, with the key absent from HTML, JS, import.meta.env, browser storage and the live DOM
  • Dev-server import.meta.env payload inspected directly: contains only path, mode, api-version and model
  • prettier:check, typecheck (5 projects) and build (4 projects) pass; built bundles contain only the placeholder and the proxy path

Upgrading

Existing .env files must be migrated: rename VITE_OPENAI_API_KEY to OPENAI_API_KEY and rotate the key, since it was previously served to browsers. The dev server refuses to start until this is done.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

The unbounded request body and unresolved proxy configuration and test coverage issues must be addressed.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Moves OpenAI credentials out of browser bundles by introducing a Vite-hosted reverse proxy and updating both reference apps.

Changes:

  • Adds OpenAI/Azure credential injection, forwarding, and streaming.
  • Migrates chat and image apps to server-only credentials.
  • Updates image generation to gpt-image-1.
File summaries
File Description Review
yarn.lock Registers proxy dependencies. No issues.
README.md Documents secure configuration and migration. Nit (2 votes), line 104: Correct the migration note: the legacy key prevents startup rather than warning. Nit (1 vote), line 153: Include OPENAI_IMAGE_MODEL among injected client values.
packages/promptions-openai-proxy/tsconfig.json Configures proxy type-checking. No issues.
packages/promptions-openai-proxy/src/index.js Implements proxy configuration and forwarding. Critical (1 vote), line 225: Bound or stream request bodies to prevent unbounded memory consumption. Moderate (1 vote), line 114: Prevent build-time client settings from diverging from preview-time proxy settings.
packages/promptions-openai-proxy/src/index.d.ts Exposes plugin API types. No issues.
packages/promptions-openai-proxy/README.md Documents proxy usage and limitations. No issues.
packages/promptions-openai-proxy/project.json Defines the Nx project. Moderate (1 vote), line 7: Commit the claimed security-sensitive tests and expose a test target.
packages/promptions-openai-proxy/package.json Defines the proxy package. No issues.
apps/promptions-image/vite.config.ts Enables the proxy plugin. No issues.
apps/promptions-image/src/vite-env.d.ts Declares injected proxy settings. No issues.
apps/promptions-image/src/types.ts Updates image parameter types. No issues.
apps/promptions-image/src/services/ImageService.ts Routes API requests through the proxy. No issues.
apps/promptions-image/src/App.tsx Selects gpt-image-1. No issues.
apps/promptions-image/package.json Adds the proxy dependency. No issues.
apps/promptions-image/.env.example Documents server-side configuration. No issues.
apps/promptions-chat/vite.config.ts Enables the proxy plugin. No issues.
apps/promptions-chat/src/vite-env.d.ts Declares injected proxy settings. No issues.
apps/promptions-chat/src/services/ChatService.ts Routes chat requests through the proxy. No issues.
apps/promptions-chat/README.md Updates setup and security guidance. No issues.
apps/promptions-chat/package.json Adds the proxy dependency. No issues.
apps/promptions-chat/.env.example Documents secure environment configuration. No issues.
Review details

Suppressed comments (1)

README.md:153

  • OPENAI_IMAGE_MODEL is also injected into client code by the plugin, so the parenthetical list is incomplete and contradicts the configuration table below.
Both apps read these variables from their respective `.env` files. They are read by the dev/preview server only and are never sent to the browser (except `OPENAI_API_VERSION` and `OPENAI_MODEL`, which are not secret).
  • Files reviewed: 17/21 changed files
  • Comments generated: 4
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/promptions-openai-proxy/src/index.js
Comment thread packages/promptions-openai-proxy/project.json
Comment thread packages/promptions-openai-proxy/src/index.js Outdated
Comment thread README.md Outdated
Jack Williams and others added 6 commits September 3, 2026 15:07
VITE_OPENAI_API_KEY was inlined into the shipped JavaScript by Vite and used
client-side with dangerouslyAllowBrowser, so any visitor could extract the
credential from the served assets.

Add @promptions/promptions-openai-proxy, a Vite plugin that proxies requests
through the dev and preview servers. The key is now read from OPENAI_API_KEY
without the VITE_ prefix, which makes inlining impossible, and is attached to
requests server-side. The browser calls a same-origin /api/openai path with a
placeholder credential.

The proxy forwards the path and query verbatim, so both OpenAI and Azure
OpenAI URL shapes work unchanged, and streams responses back so SSE token
streaming is unaffected. OPENAI_API_STYLE overrides the inferred auth scheme
for OpenAI-compatible backends that need a custom base URL.

dangerouslyAllowBrowser remains set because the SDK requires it to run in a
browser at all, but there is no longer a real credential to expose.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The current OpenAI images API rejects `response_format` and no longer serves
`dall-e-3` or `dall-e-2` on new keys, so image generation failed with
`unknown_parameter` / `model does not exist`. gpt-image-1 returns base64
payloads by default, so the parameter is redundant.

Drop the DALL-E parameter type and correct the gpt-image-1 size and quality
unions to the values the API accepts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ents

Follow-up to code review of the proxy change:

- Fail startup when VITE_OPENAI_API_KEY is set. Vite serves the whole
  import.meta.env object to the browser in dev, so a key left over from
  the pre-proxy setup is still handed to every page visitor even though
  no code references it. Warn about any other credential-shaped VITE_
  variable (KEY/SECRET/TOKEN/PASSWORD).
- Add OPENAI_IMAGE_MODEL. On Azure the SDK turns the request's model into
  a deployment name, so hardcoding gpt-image-1 404s unless the deployment
  happens to share the model id.
- Send x-should-retry: false with the unconfigured-proxy 500 so the SDK
  does not turn a missing key into three requests and several seconds of
  backoff.
- Document the migration in both .env.example files and the READMEs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Cap proxied request bodies at 10 MB (413, no SDK retry) so a browser
  cannot stream an unbounded payload through the dev/preview server.
- Emit the resolved non-secret client config into the build and warn on
  �ite preview when dist/ was built with different proxy settings.
- Add a node:test suite covering config resolution, build/preview drift
  and forwarding (credential injection, auth styles, streaming, 413,
  missing key), wired into a 	est target and the CI workflow.
- Clarify the README migration note and the injected client values.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Neither package has any test files and jest is not a dependency anywhere
in the workspace, so both targets failed unconditionally. They only
surfaced now that CI runs \yarn test\.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Aligns with the 7.3.5 the apps use so the lockfile does not gain a second
vite resolution and its esbuild platform packages.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jack-williams
Jack Williams (jack-williams) merged commit 841feed into microsoft:main Sep 3, 2026
3 checks passed
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.

2 participants