Skip to content

feat(web-studio): auto-proxy mode for public deployments#2312

Draft
ZaynJarvis wants to merge 2 commits into
volcengine:mainfrom
ZaynJarvis:feat/web-studio-auto-proxy-mode
Draft

feat(web-studio): auto-proxy mode for public deployments#2312
ZaynJarvis wants to merge 2 commits into
volcengine:mainfrom
ZaynJarvis:feat/web-studio-auto-proxy-mode

Conversation

@ZaynJarvis
Copy link
Copy Markdown
Collaborator

Summary

Adds an optional auto-proxy mode to Web Studio so a single deployment can be exposed publicly without ever shipping the OpenViking root API key to the browser.

  • New zero-dep Node.js backend web-studio/server/proxy.mjs serves the built SPA and proxies /api, /bot, /health, /ready, /openapi.json to a configured upstream OV server, injecting X-API-Key (and optional X-OpenViking-Account / X-OpenViking-User) server-side.
  • Incoming X-API-Key, Authorization, X-OpenViking-Account, X-OpenViking-User are stripped before forwarding — browser callers cannot override the server-managed identity.
  • New /_studio/runtime-config.json endpoint tells the SPA it is in proxy mode. The SPA then:
    • never sends X-API-Key from the browser,
    • never persists credentials in localStorage / sessionStorage,
    • replaces the connection dialog form with a read-only "auto-proxy mode" notice,
    • skips the 401-triggered dialog reopen interceptor.

All upstream addressing and credentials live in env vars (OV_STUDIO_UPSTREAM, OV_STUDIO_API_KEY, ...). See web-studio/server/README.md for the full env reference and threat model.

Usage

cd web-studio
npm ci
npm run build

OV_STUDIO_UPSTREAM=https://ov-api.example.com \
OV_STUDIO_API_KEY=$ROOT_API_KEY \
npm run proxy   # default port 3000

Files

  • web-studio/server/proxy.mjs — proxy server (zero deps, ~325 LOC, Node built-ins only)
  • web-studio/server/README.md — env reference + threat model
  • web-studio/src/lib/studio-runtime.ts — boot-time fetch of /_studio/runtime-config.json
  • web-studio/src/lib/ov-client/{client,types}.ts — new proxyMode option that suppresses X-API-Key header injection
  • web-studio/src/hooks/use-app-connection.tsx — proxy-mode aware persistence / interceptor
  • web-studio/src/components/connection-dialog.tsx — read-only notice in proxy mode
  • web-studio/src/main.tsx — awaits runtime config before bootstrap
  • web-studio/src/i18n/locales/{en,zh-CN}.tsconnection.proxyMode.* + common.action.close
  • web-studio/{README,README_CN}.md — new "Auto-Proxy Mode" deployment section

Test plan

  • npm run lint — 0 errors (the 15 remaining warnings are all pre-existing i18next/no-literal-string in routes/resources/-components/file-preview.tsx).
  • npm run build — succeeds, bundle contains _studio/runtime-config.json and proxyMode references.
  • npm run proxy smoke test against a stub upstream:
    • GET /_studio/runtime-config.json{"proxyMode":true,...}.
    • GET / → built index.html.
    • GET /some/spa/route → SPA fallback (200 with index.html).
    • POST /api/v1/search/find with attacker-supplied X-API-Key, Authorization, X-OpenViking-Account: client headers stripped, server injects X-API-Key=injected-key + X-OpenViking-Account=acme + X-OpenViking-User=alice + X-OpenViking-Agent=web-studio-proxy; POST body forwarded intact.
    • Upstream unreachable → 502 {"status":"error","error":{"code":"UPSTREAM_UNREACHABLE",...}} (matches OV error envelope shape).
  • Manual browser run against a real openviking-server --with-bot upstream (deferred — submitting as draft for design feedback first).
  • Review whether oauth-setup-dialog UX needs a proxy-mode adjustment (currently it still renders with connection.apiKey="", which falls through to the "custom apiKey" mode — out of scope for this PR but flagged).

Design notes / open questions

  1. Where does the proxy live? Implemented as an opt-in Node script under web-studio/server/, mirroring the existing nginx layout in the README — no changes to openviking-server. Open to moving it into a separate package (@openviking/web-studio-proxy) if you'd prefer it published independently.
  2. Path prefix list. Default OV_STUDIO_PROXY_PATHS=/api,/bot,/health,/ready,/openapi.json matches what Web Studio actually calls. Future bot websocket support would need an upgrade handler — easy to add but skipped for now since openviking-server has no /ws endpoint today.
  3. WebSocket. Not needed by web-studio right now. If/when added, the proxy needs an upgrade event handler.
  4. Identity scoping. Optional OV_STUDIO_ACCOUNT_ID / OV_STUDIO_USER_ID env vars let a deployment pin to a specific account/user. Without them, the upstream applies its own default identity attached to the root key.
  5. Same-process vs. external nginx. The README keeps the nginx pattern as the recommended path when nginx is already in place. This mode is for "single container / single process" demos.

🤖 Generated with Claude Code

@github-actions
Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🏅 Score: 90
🧪 No relevant tests
🔒 No security concerns identified
✅ No TODO sections
🔀 Multiple PR themes

Sub-PR theme: Add auto-proxy server implementation

Relevant files:

  • web-studio/server/proxy.mjs
  • web-studio/server/README.md
  • web-studio/package.json

Sub-PR theme: Add proxy mode support to Web Studio frontend

Relevant files:

  • web-studio/src/lib/studio-runtime.ts
  • web-studio/src/main.tsx
  • web-studio/src/hooks/use-app-connection.tsx
  • web-studio/src/lib/ov-client/client.ts
  • web-studio/src/lib/ov-client/types.ts
  • web-studio/src/components/connection-dialog.tsx
  • web-studio/src/i18n/locales/en.ts
  • web-studio/src/i18n/locales/zh-CN.ts

Sub-PR theme: Update documentation for auto-proxy mode

Relevant files:

  • web-studio/README.md
  • web-studio/README_CN.md

⚡ Recommended focus areas for review

Missing request timeouts

The proxy server does not set timeouts for upstream requests or client connections, which could lead to hanging connections under network instability.

function proxyToUpstream(req, res) {
  const targetPath = (req.url || '/').replace(/^\/+/, '/')
  const headers = filterRequestHeaders(req.headers)
  headers['x-api-key'] = API_KEY
  if (ACCOUNT_ID) headers['x-openviking-account'] = ACCOUNT_ID
  if (USER_ID) headers['x-openviking-user'] = USER_ID
  headers['x-openviking-agent'] = 'web-studio-proxy'
  headers.host = upstreamUrl.host

  const upstream = upstreamRequest(
    {
      protocol: upstreamUrl.protocol,
      hostname: upstreamUrl.hostname,
      port: upstreamUrl.port || (upstreamUrl.protocol === 'https:' ? 443 : 80),
      method: req.method,
      path: targetPath,
      headers,
    },
    (upstreamRes) => {
      const responseHeaders = filterResponseHeaders(upstreamRes.headers)
      res.writeHead(upstreamRes.statusCode || 502, responseHeaders)
      upstreamRes.pipe(res)
    },
  )

  upstream.on('error', (error) => {
    console.error('[ov-studio-proxy] upstream error', error.message)
    if (!res.headersSent) {
      res.writeHead(502, { 'content-type': 'application/json; charset=utf-8' })
    }
    res.end(JSON.stringify({ status: 'error', error: { code: 'UPSTREAM_UNREACHABLE', message: error.message } }))
  })

  req.pipe(upstream)
}
Incomplete i18n coverage

Japanese (ja) translations are missing for the new proxy mode strings added in en and zh-CN locales.

proxyMode: {
  title: 'Auto-proxy mode',
  description:
    'A same-origin proxy is forwarding requests to the OpenViking server with a server-managed API key. The browser never sees credentials, so connection settings are locked.',
},

@github-actions
Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Handle file stream errors

Add error handling for the file read stream to avoid uncaught exceptions and ensure
a proper error response is sent to the client if reading the file fails.

web-studio/server/proxy.mjs [246]

-createReadStream(filePath).pipe(res)
+const fileStream = createReadStream(filePath)
+fileStream.on('error', (err) => {
+  console.error('[ov-studio-proxy] failed to read static file', filePath, err)
+  if (!res.headersSent) {
+    res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' })
+  }
+  res.end('Internal server error')
+})
+fileStream.pipe(res)
Suggestion importance[1-10]: 6

__

Why: The suggestion adds error handling for the static file read stream, preventing uncaught exceptions that could crash the server. This is a meaningful improvement for server stability.

Low
Add upstream request timeout

Add a timeout to the upstream proxy request to avoid hanging connections. Also,
ensure the request is properly destroyed on timeout and errors.

web-studio/server/proxy.mjs [177-191]

 const upstream = upstreamRequest(
   {
     protocol: upstreamUrl.protocol,
     hostname: upstreamUrl.hostname,
     port: upstreamUrl.port || (upstreamUrl.protocol === 'https:' ? 443 : 80),
     method: req.method,
     path: targetPath,
     headers,
+    timeout: 30000, // 30 seconds
   },
   (upstreamRes) => {
     const responseHeaders = filterResponseHeaders(upstreamRes.headers)
     res.writeHead(upstreamRes.statusCode || 502, responseHeaders)
     upstreamRes.pipe(res)
   },
 )
 
+upstream.on('timeout', () => {
+  console.error('[ov-studio-proxy] upstream request timed out')
+  upstream.destroy()
+  if (!res.headersSent) {
+    res.writeHead(504, { 'content-type': 'application/json; charset=utf-8' })
+  }
+  res.end(JSON.stringify({ status: 'error', error: { code: 'UPSTREAM_TIMEOUT', message: 'Upstream request timed out' } }))
+})
+
Suggestion importance[1-10]: 5

__

Why: The suggestion adds a timeout to the upstream proxy request to avoid hanging connections, which enhances server reliability. It's a valuable improvement for a network-facing component.

Low

A new optional Node.js backend (`web-studio/server/proxy.mjs`) and a runtime
config endpoint (`/_studio/runtime-config.json`) let Web Studio be deployed
as a public site without exposing the OpenViking root API key to the browser:

- Browser talks to a same-origin proxy with no `X-API-Key` of its own.
- Proxy injects server-managed `X-API-Key` (and optional account / user)
  before forwarding to an upstream OV server configured via env vars.
- Incoming `X-API-Key`, `Authorization`, `X-OpenViking-Account`, and
  `X-OpenViking-User` headers are stripped before forwarding so clients
  cannot override the server-managed identity.
- The SPA fetches `/_studio/runtime-config.json` at boot; when `proxyMode`
  is true it hides the connection dialog form, suppresses 401-driven
  reopen, and stops persisting credentials in `localStorage` /
  `sessionStorage`. The dialog shows a read-only "auto-proxy mode" notice.

Zero-dep Node script + ~60 lines of SPA wiring. nginx layout in the main
README remains the preferred answer when nginx is already in the path; this
mode targets single-process "static frontend + thin proxy" deployments.
- `Dockerfile`: multi-stage `node:22-alpine` image (~150 MB). Build stage runs
  `npm ci` + `npm run build`; runtime stage ships only `dist/`, `server/`,
  and `package.json`. Zero-dep proxy means no production node_modules.
- `.dockerignore`: keeps node_modules / dist / .git / docs out of the build
  context.
- `railway.toml`: pins Dockerfile build, healthcheck on
  `/_studio/runtime-config.json`, restart-on-failure with 5 retries.
- `server/proxy.mjs`: honor `PORT` env (Railway / Fly / Render / Heroku
  convention) before falling back to `OV_STUDIO_PORT` / `3000`.
- `server/README.md`: Docker + Railway / Fly / Render quick-start sections.

Local docker build is blocked by the host machine's full disk and corrupted
docker daemon, so the image is unverified locally — Railway will build it
server-side. The Dockerfile is small and standard, and the proxy.mjs has
already been smoke-tested with the `PORT` env var.
@ZaynJarvis ZaynJarvis force-pushed the feat/web-studio-auto-proxy-mode branch from de68539 to 1599f4c Compare May 29, 2026 12:01
@ZaynJarvis
Copy link
Copy Markdown
Collaborator Author

Rebased onto current main (53 commits ahead). Two conflict points:

  1. web-studio/src/lib/ov-client/client.ts — main added connection.agentId for the X-OpenViking-Agent header. Resolved by keeping the proxy-mode if/else block but using connection.agentId || WEB_STUDIO_AGENT_ID on the agent header.
  2. web-studio/src/hooks/use-app-connection.tsx — main added agentId to ConnectionDraft / setConnection. Resolved by forwarding connection.agentId even in proxy mode (the agent string is identity, not credentials) while still nulling accountId/apiKey/userId. Also extended buildProxyConnection() with agentId: DEFAULT_CONNECTION.agentId.

Verified after rebase: npm run lint 0 errors, npm run build succeeds.


Also added a Dockerfile + Railway deploy path so this can be smoke-tested end-to-end:

  • web-studio/Dockerfile — multi-stage node:22-alpine, ~150 MB runtime image (build stage runs npm ci + npm run build; runtime stage ships dist/ + server/ only — zero-dep proxy so no production node_modules).
  • web-studio/.dockerignore — drops node_modules, dist, .git, *.log.
  • web-studio/railway.toml — Dockerfile build + healthcheck on /_studio/runtime-config.json.
  • server/proxy.mjs — honors PORT env first (Railway / Fly / Render / Heroku convention) before falling back to OV_STUDIO_PORT / 3000. Verified locally.
  • server/README.md — Docker + Railway / Fly quick-start sections.

Railway path (per server/README.md): new service from this branch → Settings → Root Directory = web-studio → variables OV_STUDIO_UPSTREAM + OV_STUDIO_API_KEY → deploy. Railway injects $PORT automatically.

Note: local docker build couldn't be run against this Dockerfile because the host machine's data volume is at 100% and the local docker daemon's containerd metadata is reporting I/O errors — the Dockerfile itself is small and standard, but the runtime image is unverified locally. Railway / GHA will build it server-side.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

1 participant