Skip to content

feat: universal proxy (new approach)#837

Open
cjroth wants to merge 50 commits into
mainfrom
cjroth/universal-proxy
Open

feat: universal proxy (new approach)#837
cjroth wants to merge 50 commits into
mainfrom
cjroth/universal-proxy

Conversation

@cjroth
Copy link
Copy Markdown
Member

@cjroth cjroth commented May 6, 2026

Note

High Risk
High risk because it changes security-sensitive proxy/CORS behaviour (SSRF protections, header forwarding, redirect handling) and introduces a new WS proxy path; mistakes could leak credentials or open SSRF vectors.

Overview
Switches the backend to a new universal proxy implementation: /v1/proxy now supports header passthrough via X-Proxy-Passthrough-*, redirect controls, streaming with caps/timeouts, SSRF/DNS pinning, and adds a new /v1/proxy/ws relay with subprotocol-encoded targets.

Adds proxy observability via proxy_request/proxy_ws_relay structured logs and OpenTelemetry span attributes, explicitly redacting full URLs/credentials while recording hostname, bytes, duration, status, and categorical error types.

Introduces new authenticated APIs: /v1/search (Exa-backed, normalises results to HTTPS and derives favicons) and /v1/preview (POST-only OG metadata extraction with HTTPS image upgrade and per-user 10‑minute private caching), plus new e2e tests.

Removes the old /pro/proxy, /pro/link-preview, and /mcp-proxy routes and their tests, updates CORS to allowedHeaders: true (echo requested headers) and refreshes corsExposeHeaders defaults/docs/env examples accordingly; also tweaks auth creation for test DI, sets Better Auth baseURL, and adjusts dev tooling/lint globals/logger exports.

Reviewed by Cursor Bugbot for commit c214826. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

Semgrep Security Scan

No security issues found.

Comment thread backend/src/db/client.ts Fixed
Comment thread backend/src/proxy/routes.ts Outdated
Comment thread backend/src/api/search.ts Outdated
Comment thread backend/src/api/search.ts Outdated
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

PR Metrics

Metric Value
Lines changed (prod code) +2289 / -1266
JS bundle size (gzipped) 🟢 1.02 MB → 1.02 MB (-3.5 KB, -0.3%)
Test coverage 🟢 71.47% → 72.09% (+0.6%)
Performance (preview) Preview not ready — Render deploy may have timed out
Accessibility
Best Practices
SEO

Updated Thu, 14 May 2026 19:35:22 GMT · run #1499

Comment thread backend/src/proxy/routes.ts Outdated
cjroth added 2 commits May 6, 2026 01:16
- inject DnsLookup into url-validation, proxy, and preview routes
- inject SearchExaClient into search route via createApp deps
- inject email senders into createAuth to capture OTPs per test
- avoids mock.module leaking across test files
Comment thread backend/src/api/preview.ts
- store test PGlite instance on globalThis so it survives module reloads
- close on process beforeExit instead of afterAll, so reruns reuse one
  instance rather than reinitializing (and re-running migrations) each time
Comment thread backend/src/config/logger.ts Outdated
Comment thread backend/src/config/settings.ts Outdated
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

Preview environment deployed 🚀

Service URL
Marketing / blog / docs https://thunderbolt-pr-837.preview.thunderbolt.io
App https://app-pr-837.preview.thunderbolt.io
API https://api-pr-837.preview.thunderbolt.io
Keycloak https://auth-pr-837.preview.thunderbolt.io
PowerSync https://powersync-pr-837.preview.thunderbolt.io

Stack: preview-pr-837 · Commit: c214826744a06192c9ad5f931e1af6c49d79c2b2

Auto-destroys on PR close/merge. Login via the bundled Keycloak realm — demo@thunderbolt.io / demo by default.

- Eliminates a sign-in race where the bearer is returned before the
  session row is visible, which surfaced as a confusing 401 on the
  first authenticated request in tests.
- move cleanup into bun:test afterAll so it runs inside the test
  runner lifecycle, before Bun terminates worker threads
- drop the globalThis-shared PGlite instance; one instance per
  process is sufficient now that cleanup is reliable
ital0 added 8 commits May 12, 2026 19:41
…hrough)

- observability: strip PostHog (Pino + OTel only); add ProxyErrorType
  union and `trace.getActiveSpan().setAttributes(...)` for proxy.* attrs.
  Wire real status / duration_ms / bytes_in / bytes_out from the response
  and capStream (fixes bot MEDIUM findings: hardcoded status, zero
  duration/bytes). Tag error_type on every failure path.
- content-encoding passthrough: pass `decompress: false` to Bun fetch,
  remove `content-encoding` from DROPPED_RESPONSE_HEADERS. Browser now
  decodes; the proxy holds compressed bytes only. Comment near 10MB cap
  documents the zip-bomb risk shift to the caller.
- streaming: capStream returns { stream, bytesRead() } and fires a
  single `onComplete(bytes)` so observability emits exactly once after
  stream drain (graceful, cap-hit, idle, error, or cancel).
For streaming POSTs (no follow-redirects), bytesIn was captured at the
moment fetchFn() returned response headers — before the request body
had fully drained through capStream. Replace the captured number with
a late-read getter so the observability emission (which fires from the
response stream's onComplete) sees the final upload byte count.
- routes.ts: replace unbounded arrayBuffer() in buffered-body path with
  bounded streaming accumulator that early-terminates at maxBodyBytes.
  Closes a DoS vector where a chunked upload (no Content-Length) would
  materialise the full body in memory before the 413 check fired.
- routes.ts: document Bun's decompress:false semantics (Bun >=1.3 keeps
  content-encoding on the Response even after auto-decode, so the cast
  is load-bearing for correctness — not just a perf flag).
- routes.test.ts: add streaming-POST bytes_in regression test exercising
  the validator's deferred-getter fix (commit 7d5ddada) directly.
- routes.test.ts: add chunked-upload DoS test proving early-termination.
Comment thread backend/src/api/search.ts
Comment thread backend/src/config/settings.ts
Comment thread backend/src/index.ts
ital0 added 2 commits May 13, 2026 15:51
- Remove backend/scripts/dev.sh (Chris flagged out-of-scope)
- Revert backend/src/db/client.ts dev rewrite
- Revert playwright.config.ts tweak
- Revert powersync localhost defaults in settings.ts (closes HIGH bot finding)

These items moved to italomenezes/cjroth-proxy-dev-env (stacked PR).
# Conflicts:
#	backend/.env.example
#	backend/eslint.config.js
#	backend/src/auth/auth.ts
#	backend/src/matchers.d.ts
#	backend/src/pro/link-preview.ts
#	backend/src/proxy/routes.ts
#	backend/src/utils/streaming.ts
#	backend/src/utils/url-validation.ts
#	powersync-service/docker-compose.yml
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.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 94342ac. Configure here.

Comment thread backend/src/config/settings.ts
Comment thread src/ai/fetch.ts Outdated
}

/**
* Memoized `proxyFetch` keyed on `cloudUrl`. Construction is cheap, but creating
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think we should move this into React context instead of just let vars

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Also, what is the use case for having multiple cloud URLs at the same time? I feel this could be simplified by having a simple useState inside of the React context that is added in this commit.

Comment thread src/ai/fetch.ts
return proxyFetch
}

/** Test-only: clears the module-scoped proxy-fetch cache so tests start from a known state. */
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This could be removed if we can use useState

Comment thread src/ai/fetch.ts Outdated
Comment thread src/lib/proxy-fetch.ts Outdated

import { fetch as tauriFetch } from '@tauri-apps/plugin-http'
import {
PASSTHROUGH_PREFIX,
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Still here

Comment thread src/settings/preferences.tsx
Comment thread src/settings/preferences.tsx Outdated
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.

3 participants