Skip to content

Use dev proxy for loopback auth and environment requests#1853

Merged
juliusmarminge merged 2 commits intomainfrom
feature/dev-proxy-loopback
Apr 9, 2026
Merged

Use dev proxy for loopback auth and environment requests#1853
juliusmarminge merged 2 commits intomainfrom
feature/dev-proxy-loopback

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented Apr 9, 2026

Summary

  • Route local dev loopback auth and environment bootstrap requests through the Vite proxy when the desktop app and browser are both on loopback hosts.
  • Add proxy handling for /.well-known alongside /api so primary environment discovery works through the dev server.
  • Prevent terminal drawers from reopening when the scoped thread reference is logically unchanged, and avoid replaying terminal events for stale archived threads.
  • Add coverage for the new URL resolution, terminal event filtering, and terminal drawer stability behavior.

Testing

  • bun fmt
  • bun lint
  • bun typecheck
  • bun run test

Note

Medium Risk
Changes primary environment URL resolution and terminal event handling, which can impact local auth/bootstrap connectivity and terminal UX if the new loopback/dev-server detection is wrong. Scope is contained to local-dev proxying and terminal state updates, with added tests to reduce regressions.

Overview
Routes local-dev loopback bootstrap/auth HTTP calls through the Vite dev server proxy when appropriate. resolvePrimaryEnvironmentHttpUrl now rewrites loopback desktop-managed targets to the current dev-server origin when VITE_DEV_SERVER_URL matches the current origin and both hosts are loopback, and vite.config.ts adds a proxy rule for /.well-known to support environment discovery.

Stabilizes terminal rendering and event replay behavior. TerminalViewport no longer tears down/reopens when the threadRef object identity changes (only when environmentId changes), ChatView memoizes persistent terminal drawers and pre-parses mounted thread refs, and runtime terminal events are now applied for draft-only threads while still dropping events for archived server threads via shouldApplyTerminalEvent.

Adds test coverage for the new dev-proxy URL behavior, terminal viewport stability on rerender, and terminal event filtering.

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

Note

Route loopback auth and environment requests through the Vite dev server proxy

  • During local development, HTTP requests targeting a loopback host are rewritten to use the current dev server origin for proxying, via a new resolveHttpRequestBaseUrl utility in target.ts.
  • This affects both auth session fetches (/api/auth/session) and primary environment descriptor fetches, routing them through the Vite proxy instead of directly to the loopback server.
  • Adds /.well-known to the Vite dev server proxy rules in vite.config.ts alongside existing /api and /attachments proxies.
  • Exports isLoopbackHostname from the primary environments module and removes a duplicate local implementation in ConnectionsSettings.tsx.
  • Fixes terminal teardown in TerminalViewport by depending on environmentId value rather than threadRef object identity, preventing unnecessary terminal remounts.

Macroscope summarized f4d89f8.

- Route local desktop-managed bootstrap and auth requests through the Vite proxy
- Preserve terminal events for draft-only threads and avoid reopening drawers on stable refs
- Add tests for the new runtime terminal-event gating and proxy behavior
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e794799c-2efe-43bc-9c13-0d78077933de

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/dev-proxy-loopback

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Apr 9, 2026
Copy link
Copy Markdown
Contributor

@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 prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Duplicate loopback hostname check across files
    • Extracted the duplicated isLoopbackHostname logic into a shared utility at @t3tools/shared/hostname and updated all three consumers (apps/server/src/http.ts, apps/web/src/environments/primary/target.ts, apps/web/src/components/settings/ConnectionsSettings.tsx) to import from it.

Create PR

Or push these changes by commenting:

@cursor push a07754f7f2
Preview (a07754f7f2)
diff --git a/apps/server/src/http.test.ts b/apps/server/src/http.test.ts
--- a/apps/server/src/http.test.ts
+++ b/apps/server/src/http.test.ts
@@ -1,7 +1,9 @@
 import { describe, expect, it } from "vitest";
 
-import { isLoopbackHostname, resolveDevRedirectUrl } from "./http.ts";
+import { isLoopbackHostname } from "@t3tools/shared/hostname";
 
+import { resolveDevRedirectUrl } from "./http.ts";
+
 describe("http dev routing", () => {
   it("treats localhost and loopback addresses as local", () => {
     expect(isLoopbackHostname("127.0.0.1")).toBe(true);

diff --git a/apps/server/src/http.ts b/apps/server/src/http.ts
--- a/apps/server/src/http.ts
+++ b/apps/server/src/http.ts
@@ -11,6 +11,8 @@
 } from "effect/unstable/http";
 import { OtlpTracer } from "effect/unstable/observability";
 
+import { isLoopbackHostname } from "@t3tools/shared/hostname";
+
 import {
   ATTACHMENTS_ROUTE_PREFIX,
   normalizeAttachmentRelativePath,
@@ -28,7 +30,6 @@
 const PROJECT_FAVICON_CACHE_CONTROL = "public, max-age=3600";
 const FALLBACK_PROJECT_FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#6b728080" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-fallback="project-favicon"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-8l-2-2H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2Z"/></svg>`;
 const OTLP_TRACES_PROXY_PATH = "/api/observability/v1/traces";
-const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "::1", "localhost"]);
 
 export const browserApiCorsLayer = HttpRouter.cors({
   allowedMethods: ["GET", "POST", "OPTIONS"],
@@ -36,14 +37,6 @@
   maxAge: 600,
 });
 
-export function isLoopbackHostname(hostname: string): boolean {
-  const normalizedHostname = hostname
-    .trim()
-    .toLowerCase()
-    .replace(/^\[(.*)\]$/, "$1");
-  return LOOPBACK_HOSTNAMES.has(normalizedHostname);
-}
-
 export function resolveDevRedirectUrl(devUrl: URL, requestUrl: URL): string {
   const redirectUrl = new URL(devUrl.toString());
   redirectUrl.pathname = requestUrl.pathname;

diff --git a/apps/web/src/components/settings/ConnectionsSettings.tsx b/apps/web/src/components/settings/ConnectionsSettings.tsx
--- a/apps/web/src/components/settings/ConnectionsSettings.tsx
+++ b/apps/web/src/components/settings/ConnectionsSettings.tsx
@@ -46,6 +46,8 @@
 import { Button } from "../ui/button";
 import { Textarea } from "../ui/textarea";
 import { setPairingTokenOnUrl } from "../../pairingUrl";
+import { isLoopbackHostname } from "@t3tools/shared/hostname";
+
 import {
   createServerPairingCredential,
   fetchSessionState,
@@ -253,16 +255,6 @@
   return setPairingTokenOnUrl(url, credential).toString();
 }
 
-function isLoopbackHostname(hostname: string): boolean {
-  const normalized = hostname.trim().toLowerCase();
-  return (
-    normalized === "localhost" ||
-    normalized === "127.0.0.1" ||
-    normalized === "::1" ||
-    normalized === "[::1]"
-  );
-}
-
 type PairingLinkListRowProps = {
   pairingLink: ServerPairingLinkRecord;
   endpointUrl: string | null | undefined;

diff --git a/apps/web/src/environments/primary/target.ts b/apps/web/src/environments/primary/target.ts
--- a/apps/web/src/environments/primary/target.ts
+++ b/apps/web/src/environments/primary/target.ts
@@ -1,13 +1,12 @@
 import type { DesktopEnvironmentBootstrap } from "@t3tools/contracts";
 import type { KnownEnvironment } from "@t3tools/client-runtime";
+import { isLoopbackHostname } from "@t3tools/shared/hostname";
 
 export interface PrimaryEnvironmentTarget {
   readonly source: KnownEnvironment["source"];
   readonly target: KnownEnvironment["target"];
 }
 
-const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "::1", "localhost"]);
-
 function getDesktopLocalEnvironmentBootstrap(): DesktopEnvironmentBootstrap | null {
   return window.desktopBridge?.getLocalEnvironmentBootstrap() ?? null;
 }
@@ -16,17 +15,6 @@
   return new URL(rawValue, window.location.origin).toString();
 }
 
-function normalizeHostname(hostname: string): string {
-  return hostname
-    .trim()
-    .toLowerCase()
-    .replace(/^\[(.*)\]$/, "$1");
-}
-
-function isLoopbackHostname(hostname: string): boolean {
-  return LOOPBACK_HOSTNAMES.has(normalizeHostname(hostname));
-}
-
 function resolveHttpRequestBaseUrl(httpBaseUrl: string): string {
   const configuredDevServerUrl = import.meta.env.VITE_DEV_SERVER_URL?.trim();
   if (!configuredDevServerUrl) {

diff --git a/packages/shared/package.json b/packages/shared/package.json
--- a/packages/shared/package.json
+++ b/packages/shared/package.json
@@ -51,6 +51,10 @@
     "./projectScripts": {
       "types": "./src/projectScripts.ts",
       "import": "./src/projectScripts.ts"
+    },
+    "./hostname": {
+      "types": "./src/hostname.ts",
+      "import": "./src/hostname.ts"
     }
   },
   "scripts": {

diff --git a/packages/shared/src/hostname.ts b/packages/shared/src/hostname.ts
new file mode 100644
--- /dev/null
+++ b/packages/shared/src/hostname.ts
@@ -1,0 +1,12 @@
+const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "::1", "localhost"]);
+
+function normalizeHostname(hostname: string): string {
+  return hostname
+    .trim()
+    .toLowerCase()
+    .replace(/^\[(.*)\]$/, "$1");
+}
+
+export function isLoopbackHostname(hostname: string): boolean {
+  return LOOPBACK_HOSTNAMES.has(normalizeHostname(hostname));
+}

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 04e211a. Configure here.

macroscopeapp[bot]
macroscopeapp bot previously approved these changes Apr 9, 2026
@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp bot commented Apr 9, 2026

Approvability

A prior Macroscope approval was dismissed. Re-evaluating f4d89f8

@juliusmarminge juliusmarminge enabled auto-merge (squash) April 9, 2026 17:50
@macroscopeapp macroscopeapp bot dismissed their stale review April 9, 2026 17:51

Dismissing prior approval to re-evaluate f4d89f8

@juliusmarminge juliusmarminge merged commit 5b3b31b into main Apr 9, 2026
12 checks passed
@juliusmarminge juliusmarminge deleted the feature/dev-proxy-loopback branch April 9, 2026 17:53
rororowyourboat added a commit to rororowyourboat/t3code that referenced this pull request Apr 9, 2026
…threadId (#2)

* Raise slow RPC ack warning threshold to 15s (pingdotgg#1760)

* Use active worktree path for workspace saves (pingdotgg#1762)

* Stream git status updates over WebSocket (pingdotgg#1763)

Co-authored-by: codex <codex@users.noreply.github.com>

* fix(web): unwrap windows shell command wrappers (pingdotgg#1719)

* Rename "Chat" to "Build" in interaction mode toggle (pingdotgg#1769)

Co-authored-by: Julius Marminge <julius0216@outlook.com>

* Assign default capabilities to Codex custom models (pingdotgg#1793)

* Add project rename support in the sidebar (pingdotgg#1798)

* Support multi-select pending user inputs (pingdotgg#1797)

* Add Zed support to Open actions via editor command aliases (pingdotgg#1303)

Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: Julius Marminge <julius0216@outlook.com>

* Closes pingdotgg#1795 - Support building and developing in a devcontainer (pingdotgg#1791)

* Add explicit timeouts to CI and release workflows (pingdotgg#1825)

* fix(web): distinguish singular/plural in pending action submit label (pingdotgg#1826)

* Refactor web stores into atomic slices ready to split ChatView (pingdotgg#1708)

* Add VSCode Insiders and VSCodium icons (pingdotgg#1847)

* Prepare datamodel for multi-environment (pingdotgg#1765)

Co-authored-by: justsomelegs <145564979+justsomelegs@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cursor[bot] <206951365+cursor[bot]@users.noreply.github.com>

* Implement server auth bootstrap and pairing flow (pingdotgg#1768)

Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: Julius Marminge <julius@macmini.local>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cursor[bot] <206951365+cursor[bot]@users.noreply.github.com>

* Use dev proxy for loopback auth and environment requests (pingdotgg#1853)

* Refresh local git status on turn completion (pingdotgg#1821)

Co-authored-by: codex <codex@users.noreply.github.com>

* fix(desktop): add Copy Link action for chat links (pingdotgg#1835)

* fix: map runtime modes to correct permission levels (pingdotgg#1587)

Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>

* Fix persisted composer image hydration typo (pingdotgg#1831)

* Clarify environment and workspace picker labels (pingdotgg#1854)

* Scope git toast state by thread ref (pingdotgg#1855)

* fix build (pingdotgg#1859)

* Stabilize keybindings toast stream setup (pingdotgg#1860)

Co-authored-by: Julius Marminge <julius@macmini.local>

* feat(web): add embeddable thread route for canvas tile hosts

Adds /embed/thread/:environmentId/:threadId — a standalone route that
renders the existing ChatView without the app sidebar chrome. This is the
iframe target for t3-canvas agent shapes (see rororowyourboat/t3-canvas#3).

- New file-based route embed.thread.\$environmentId.\$threadId.tsx
- __root.tsx bypasses AppSidebarLayout for any /embed/* pathname so the
  environment connection + websocket surface + toasts still initialize
  but the sidebar/diff/plan chrome does not render
- minimal=1 search param is parsed and wired to a data attribute on the
  container for future targeted CSS; chrome hiding (BranchToolbar,
  PlanSidebar, ThreadTerminalDrawer) stays as a follow-up pass
- routeTree.gen.ts regenerated by the @tanstack/router-plugin

---------

Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: legs <145564979+justsomelegs@users.noreply.github.com>
Co-authored-by: sonder <168988030+heysonder@users.noreply.github.com>
Co-authored-by: Adem Ben Abdallah <96244394+AdemBenAbdallah@users.noreply.github.com>
Co-authored-by: Kyle Gottfried <6462596+Spitfire1900@users.noreply.github.com>
Co-authored-by: Jacob <589761+jvzijp@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cursor[bot] <206951365+cursor[bot]@users.noreply.github.com>
Co-authored-by: Julius Marminge <julius@macmini.local>
Co-authored-by: Klemencina <56873773+Klemencina@users.noreply.github.com>
Co-authored-by: Oskar Sekutowicz <me.oski646@gmail.com>
Co-authored-by: Noxire <59626436+noxire-dev@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant