From d715190bb03da80da933d562129d8a76998eaa4d Mon Sep 17 00:00:00 2001 From: ggdayup Date: Sat, 19 Sep 2026 11:04:02 +0800 Subject: [PATCH] fix(runtime): eliminate hardcoded 20128 port remnants and make loopback URLs dynamic (#13533) * fix(runtime): eliminate hardcoded 20128 port remnants and make loopback URLs dynamic - Make model assessment probe base URL resolve dynamically from getRuntimePorts() / env - Support dynamic loopback in traffic inspector replay route and MITM handlers - Make WebSocket live server allowlist dynamically include runtime PORT/DASHBOARD_PORT loopback origins - Update CLI tools config/apply/letta-settings and tool-detector to adapt to configured runtime port - Update client UI components (EndpointPageClient, ApiExplorerClient, RelayProxyClient) to use current window origin or dynamic port - Make resolveOmniRouteBaseUrl, useDisplayBaseUrl, and wellKnown.ts respect configured port - Update package.json electron:dev wait-on to use ${PORT:-20128} - Add test coverage for custom port in resolveOmniRouteBaseUrl and liveServerAllowList - Add changelog fragment for PR #13533 * fix(runtime): complete the truncated fallback comment in wellKnown.ts Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- .../13533-dynamic-loopback-port-remnants.md | 1 + package.json | 2 +- .../dashboard/endpoint/EndpointPageClient.tsx | 4 +++- .../dashboard/relay/RelayProxyClient.tsx | 4 +++- .../dashboard/tools/agent-bridge/page.tsx | 3 ++- src/app/api/assess/route.ts | 20 ++++++++++++---- src/app/api/cli-tools/apply/route.ts | 8 ++++++- src/app/api/cli-tools/config/route.ts | 12 ++++++++-- src/app/api/cli-tools/letta-settings/route.ts | 23 ++++++++----------- .../api/playground/improve-prompt/route.ts | 2 +- src/app/api/sync/cloud/route.ts | 3 ++- .../requests/[id]/replay/route.ts | 11 +++++++-- src/app/docs/components/ApiExplorerClient.tsx | 4 +++- src/domain/assessment/assessor.ts | 4 +++- src/lib/cli-helper/log-streamer.ts | 7 +++++- src/lib/cli-helper/tool-detector.ts | 10 ++++++-- src/lib/wellKnown.ts | 3 ++- src/mitm/handlers/base.ts | 4 +++- src/mitm/server.cjs | 2 +- src/server/origin/publicOrigin.ts | 10 ++++---- src/server/ws/liveServerAllowList.ts | 12 +++++++++- src/shared/hooks/useDisplayBaseUrl.ts | 6 ++++- src/shared/utils/resolveOmniRouteBaseUrl.ts | 8 ++++++- tests/unit/resolve-omniroute-base-url.test.ts | 4 ++++ .../security/live-server-allowlist.test.ts | 17 ++++++++++++++ 25 files changed, 139 insertions(+), 45 deletions(-) create mode 100644 changelog.d/fixes/13533-dynamic-loopback-port-remnants.md diff --git a/changelog.d/fixes/13533-dynamic-loopback-port-remnants.md b/changelog.d/fixes/13533-dynamic-loopback-port-remnants.md new file mode 100644 index 0000000000..5ff47ceff6 --- /dev/null +++ b/changelog.d/fixes/13533-dynamic-loopback-port-remnants.md @@ -0,0 +1 @@ +- **fix(runtime):** eliminate hardcoded 20128 port remnants and make loopback URLs dynamic ([#13533](https://github.com/diegosouzapw/OmniRoute/pull/13533)) — thanks @ggdayup diff --git a/package.json b/package.json index 591c0f5b7d..f024801316 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,7 @@ "lint:json": "node scripts/quality/run-eslint-json.mjs", "lint:md": "npx --yes markdownlint-cli2 \"docs/**/*.md\" \"*.md\" \"!docs/i18n\" \"!docs/research\"", "lint:prose": "vale docs", - "electron:dev": "concurrently \"npm run dev\" \"wait-on http://localhost:20128 && cd electron && npm run dev\"", + "electron:dev": "concurrently \"npm run dev\" \"wait-on http://localhost:${PORT:-20128} && cd electron && npm run dev\"", "electron:build": "npm run build && cd electron && npm run build", "electron:build:win": "npm run build && cd electron && npm run build:win", "electron:build:mac": "npm run build && cd electron && npm run build:mac", diff --git a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx index cd869f9f56..dc25ec008a 100644 --- a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx +++ b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.tsx @@ -168,7 +168,9 @@ export default function APIPageClient({ machineId }: Readonly(null); - const [localApiUrl, setLocalApiUrl] = useState("http://localhost:20128/v1"); + const [localApiUrl, setLocalApiUrl] = useState( + typeof window !== "undefined" ? `${window.location.origin}/v1` : "http://localhost:20128/v1" + ); const [lanUrls, setLanUrls] = useState([]); const [tailscaleIpUrl, setTailscaleIpUrl] = useState(null); const [activeEndpointTab, setActiveEndpointTab] = useState("apis"); diff --git a/src/app/(dashboard)/dashboard/relay/RelayProxyClient.tsx b/src/app/(dashboard)/dashboard/relay/RelayProxyClient.tsx index 4ad9050e5f..5c5eab612c 100644 --- a/src/app/(dashboard)/dashboard/relay/RelayProxyClient.tsx +++ b/src/app/(dashboard)/dashboard/relay/RelayProxyClient.tsx @@ -6,6 +6,7 @@ import Card from "@/shared/components/Card"; import Badge from "@/shared/components/Badge"; import Button from "@/shared/components/Button"; import { useNotificationStore } from "@/store/notificationStore"; +import { useDisplayBaseUrl } from "@/shared/hooks"; interface RelayToken { id: string; @@ -23,6 +24,7 @@ interface RelayToken { export default function RelayProxyClient() { const t = useTranslations("relay"); + const displayBaseUrl = useDisplayBaseUrl(); const [tokens, setTokens] = useState([]); const [loading, setLoading] = useState(true); const [showCreate, setShowCreate] = useState(false); @@ -193,7 +195,7 @@ export default function RelayProxyClient() {

{t("usage")}

{t("usageDescription")}

-            {`curl http://localhost:20128/v1/relay/chat/completions \\
+            {`curl ${displayBaseUrl}/v1/relay/chat/completions \\
   -H "Authorization: Bearer relay_..." \\
   -H "Content-Type: application/json" \\
   -d '{"model":"claude-sonnet-4","messages":[{"role":"user","content":"Hello"}]}'`}
diff --git a/src/app/(dashboard)/dashboard/tools/agent-bridge/page.tsx b/src/app/(dashboard)/dashboard/tools/agent-bridge/page.tsx
index e5decf84c5..dec2e60786 100644
--- a/src/app/(dashboard)/dashboard/tools/agent-bridge/page.tsx
+++ b/src/app/(dashboard)/dashboard/tools/agent-bridge/page.tsx
@@ -41,7 +41,8 @@ export default async function AgentBridgePage() {
   try {
     const base =
       process.env.OMNIROUTE_BASE_URL ??
-      `http://127.0.0.1:${process.env.PORT ?? 20128}`;
+      process.env.BASE_URL ??
+      `http://127.0.0.1:${process.env.DASHBOARD_PORT ?? process.env.PORT ?? 20128}`;
     const res = await fetch(`${base}/api/tools/agent-bridge/state`, {
       cache: "no-store",
       headers: { "x-internal-fetch": "1" },
diff --git a/src/app/api/assess/route.ts b/src/app/api/assess/route.ts
index a8c83bedd4..0dd9a7e366 100644
--- a/src/app/api/assess/route.ts
+++ b/src/app/api/assess/route.ts
@@ -11,9 +11,18 @@ import {
 import { validateBody } from "@/shared/validation/helpers";
 import { sanitizeErrorMessage } from "@omniroute/open-sse/utils/error";
 
+function getAssessBaseUrl(): string {
+  return (
+    process.env.OMNIROUTE_BASE_URL ??
+    process.env.OMNIROUTe_BASE_URL ??
+    process.env.BASE_URL ??
+    `http://localhost:${process.env.API_PORT ?? process.env.PORT ?? 20128}/v1`
+  );
+}
+
 const assessor = new Assessor(
-  process.env.OMNIROUTe_API_KEY ?? process.env.API_KEY ?? "",
-  process.env.OMNIROUTe_BASE_URL ?? "http://localhost:20128/v1"
+  process.env.OMNIROUTE_API_KEY ?? process.env.OMNIROUTe_API_KEY ?? process.env.API_KEY ?? "",
+  getAssessBaseUrl()
 );
 
 const categorizer = new Categorizer();
@@ -142,9 +151,12 @@ export async function GET(request: NextRequest) {
 
 async function getAllModels(): Promise> {
   try {
-    const resp = await fetch("http://localhost:20128/v1/models", {
+    const baseUrl = getAssessBaseUrl();
+    const apiKey =
+      process.env.OMNIROUTE_API_KEY ?? process.env.OMNIROUTe_API_KEY ?? process.env.API_KEY ?? "";
+    const resp = await fetch(`${baseUrl}/models`, {
       headers: {
-        Authorization: `Bearer ${process.env.OMNIROUTe_API_KEY ?? process.env.API_KEY ?? ""}`,
+        Authorization: `Bearer ${apiKey}`,
       },
     });
     const data = (await resp.json()) as { data?: unknown };
diff --git a/src/app/api/cli-tools/apply/route.ts b/src/app/api/cli-tools/apply/route.ts
index 303728d55d..5ef5fc0000 100644
--- a/src/app/api/cli-tools/apply/route.ts
+++ b/src/app/api/cli-tools/apply/route.ts
@@ -50,8 +50,14 @@ export async function POST(request: Request) {
     const { toolId, baseUrl, apiKey, model, dryRun } = parsed.data;
     const canonicalToolId = normalizeCliToolId(toolId);
 
+    const defaultPort = process.env.API_PORT || process.env.PORT || 20128;
+    const defaultBaseUrl =
+      process.env.OMNIROUTE_BASE_URL ||
+      process.env.BASE_URL ||
+      `http://localhost:${defaultPort}/v1`;
+
     const result = await generateConfig(canonicalToolId, {
-      baseUrl: baseUrl || "http://localhost:20128/v1",
+      baseUrl: baseUrl || defaultBaseUrl,
       apiKey,
       model,
     });
diff --git a/src/app/api/cli-tools/config/route.ts b/src/app/api/cli-tools/config/route.ts
index c986ca1db0..766b3374bd 100644
--- a/src/app/api/cli-tools/config/route.ts
+++ b/src/app/api/cli-tools/config/route.ts
@@ -16,7 +16,10 @@ export async function GET(request: Request) {
   if (authError) return authError;
 
   const { searchParams } = new URL(request.url);
-  const baseUrl = searchParams.get("baseUrl") || "http://localhost:20128/v1";
+  const defaultPort = process.env.API_PORT || process.env.PORT || 20128;
+  const defaultBaseUrl =
+    process.env.OMNIROUTE_BASE_URL || process.env.BASE_URL || `http://localhost:${defaultPort}/v1`;
+  const baseUrl = searchParams.get("baseUrl") || defaultBaseUrl;
   const apiKey = searchParams.get("apiKey") || "";
 
   if (!apiKey) {
@@ -46,9 +49,14 @@ export async function POST(request: Request) {
       );
     }
     const { toolId, baseUrl, apiKey, model } = parsed.data;
+    const defaultPort = process.env.API_PORT || process.env.PORT || 20128;
+    const defaultBaseUrl =
+      process.env.OMNIROUTE_BASE_URL ||
+      process.env.BASE_URL ||
+      `http://localhost:${defaultPort}/v1`;
 
     const result = await generateConfig(toolId, {
-      baseUrl: baseUrl || "http://localhost:20128/v1",
+      baseUrl: baseUrl || defaultBaseUrl,
       apiKey,
       model,
     });
diff --git a/src/app/api/cli-tools/letta-settings/route.ts b/src/app/api/cli-tools/letta-settings/route.ts
index f10f1a42a7..10410daca4 100644
--- a/src/app/api/cli-tools/letta-settings/route.ts
+++ b/src/app/api/cli-tools/letta-settings/route.ts
@@ -74,7 +74,13 @@ const readAuthFile = async () => {
 // ── Check if a base_url points to OmniRoute ──────────────────────────────
 const isOmniRouteUrl = (baseUrl) => {
   if (!baseUrl) return false;
-  return baseUrl.includes(":20128") || baseUrl.includes(":3000") || baseUrl.includes("omniroute");
+  const port = process.env.PORT || process.env.DASHBOARD_PORT;
+  return (
+    baseUrl.includes(":20128") ||
+    baseUrl.includes(":3000") ||
+    (!!port && baseUrl.includes(`:${port}`)) ||
+    baseUrl.includes("omniroute")
+  );
 };
 
 // ── Check if OmniRoute is configured ─────────────────────────────────────
@@ -122,10 +128,7 @@ export async function GET(request: Request) {
       backendMode: settings.preferredBackendMode || "api",
     });
   } catch (error) {
-    return NextResponse.json(
-      { error: { message: sanitizeErrorMessage(error) } },
-      { status: 500 }
-    );
+    return NextResponse.json({ error: { message: sanitizeErrorMessage(error) } }, { status: 500 });
   }
 }
 
@@ -246,10 +249,7 @@ export async function POST(request: Request) {
       needsRestart: true,
     });
   } catch (error) {
-    return NextResponse.json(
-      { error: { message: sanitizeErrorMessage(error) } },
-      { status: 500 }
-    );
+    return NextResponse.json({ error: { message: sanitizeErrorMessage(error) } }, { status: 500 });
   }
 }
 
@@ -321,9 +321,6 @@ export async function DELETE(request: Request) {
       needsRestart: true,
     });
   } catch (error) {
-    return NextResponse.json(
-      { error: { message: sanitizeErrorMessage(error) } },
-      { status: 500 }
-    );
+    return NextResponse.json({ error: { message: sanitizeErrorMessage(error) } }, { status: 500 });
   }
 }
diff --git a/src/app/api/playground/improve-prompt/route.ts b/src/app/api/playground/improve-prompt/route.ts
index daa1c9dc4c..21a2d6fe00 100644
--- a/src/app/api/playground/improve-prompt/route.ts
+++ b/src/app/api/playground/improve-prompt/route.ts
@@ -76,7 +76,7 @@ export async function POST(request: Request): Promise {
   const chatBody = buildImproveChatBody(body);
 
   // 5. Call /v1/chat/completions on ourselves (D8)
-  const port = process.env.PORT ?? "20128";
+  const port = process.env.API_PORT ?? process.env.PORT ?? "20128";
   const baseUrl = process.env.OMNIROUTE_BASE_URL ?? `http://127.0.0.1:${port}`;
   const upstreamUrl = `${baseUrl}/v1/chat/completions`;
 
diff --git a/src/app/api/sync/cloud/route.ts b/src/app/api/sync/cloud/route.ts
index 44890fc510..a198a661ce 100644
--- a/src/app/api/sync/cloud/route.ts
+++ b/src/app/api/sync/cloud/route.ts
@@ -230,7 +230,8 @@ async function handleDisable(machineId: string, request: any) {
   }
 
   // Update Claude CLI settings to use local endpoint
-  const host = request.headers.get("host") || "localhost:20128";
+  const defaultPort = process.env.PORT || process.env.DASHBOARD_PORT || "20128";
+  const host = request.headers.get("host") || `localhost:${defaultPort}`;
   await updateClaudeSettingsToLocal(machineId, host);
 
   return NextResponse.json({
diff --git a/src/app/api/tools/traffic-inspector/requests/[id]/replay/route.ts b/src/app/api/tools/traffic-inspector/requests/[id]/replay/route.ts
index 1b5253e3b3..0eb0fee492 100644
--- a/src/app/api/tools/traffic-inspector/requests/[id]/replay/route.ts
+++ b/src/app/api/tools/traffic-inspector/requests/[id]/replay/route.ts
@@ -15,7 +15,14 @@ interface Params {
   params: Promise<{ id: string }>;
 }
 
-const OMNIROUTE_BASE = process.env.OMNIROUTE_BASE_URL ?? "http://127.0.0.1:20128";
+function getOmnirouteBaseUrl(): string {
+  const port = process.env.API_PORT || process.env.PORT || 20128;
+  return (
+    process.env.OMNIROUTE_BASE_URL ||
+    process.env.BASE_URL ||
+    `http://127.0.0.1:${port}`
+  ).replace(/\/+$/, "");
+}
 
 export async function POST(_request: Request, { params }: Params): Promise {
   const { id } = await params;
@@ -27,7 +34,7 @@ export async function POST(_request: Request, { params }: Params): Promise = {
     "content-type": "application/json",
diff --git a/src/app/docs/components/ApiExplorerClient.tsx b/src/app/docs/components/ApiExplorerClient.tsx
index e1e25144d3..4ba113e0db 100644
--- a/src/app/docs/components/ApiExplorerClient.tsx
+++ b/src/app/docs/components/ApiExplorerClient.tsx
@@ -92,7 +92,9 @@ export function ApiExplorerClient() {
   const t = useTranslations("docs");
   const te = useTranslations("endpoint");
   const [selected, setSelected] = useState(null);
-  const [baseUrl, setBaseUrl] = useState("http://localhost:20128");
+  const [baseUrl, setBaseUrl] = useState(
+    typeof window !== "undefined" ? window.location.origin : "http://localhost:20128"
+  );
   const [apiKey, setApiKey] = useState("");
   const [requestBody, setRequestBody] = useState("");
   const [response, setResponse] = useState(null);
diff --git a/src/domain/assessment/assessor.ts b/src/domain/assessment/assessor.ts
index 0358f57007..163b005cc5 100644
--- a/src/domain/assessment/assessor.ts
+++ b/src/domain/assessment/assessor.ts
@@ -34,7 +34,9 @@ export class Assessor {
 
   constructor(
     apiKey: string,
-    baseUrl: string = "http://localhost:20128/v1",
+    baseUrl: string = process.env.OMNIROUTE_BASE_URL ??
+      process.env.BASE_URL ??
+      `http://localhost:${process.env.API_PORT ?? process.env.PORT ?? 20128}/v1`,
     config: Partial = {}
   ) {
     this.apiKey = apiKey;
diff --git a/src/lib/cli-helper/log-streamer.ts b/src/lib/cli-helper/log-streamer.ts
index 06dbffbb2d..2890b4a959 100644
--- a/src/lib/cli-helper/log-streamer.ts
+++ b/src/lib/cli-helper/log-streamer.ts
@@ -12,7 +12,12 @@ export interface LogStream {
 }
 
 export function createLogStream(options: LogStreamOptions = {}): LogStream {
-  const baseUrl = options.baseUrl || "http://localhost:20128";
+  const port = process.env.PORT || process.env.DASHBOARD_PORT || 20128;
+  const baseUrl =
+    options.baseUrl ||
+    process.env.OMNIROUTE_BASE_URL ||
+    process.env.BASE_URL ||
+    `http://localhost:${port}`;
   const filters = options.filters || [];
   const follow = options.follow ?? false;
   const timeout = options.timeout || 30000;
diff --git a/src/lib/cli-helper/tool-detector.ts b/src/lib/cli-helper/tool-detector.ts
index 38a2b5d289..01b90a07a7 100644
--- a/src/lib/cli-helper/tool-detector.ts
+++ b/src/lib/cli-helper/tool-detector.ts
@@ -77,9 +77,11 @@ function expandHome(p: string): string {
 
 function isConfigured(content: string, baseUrl: string): boolean {
   const normalized = baseUrl.replace(/\/+$/, "");
+  const port = process.env.PORT || process.env.DASHBOARD_PORT;
   return (
     content.includes(normalized) ||
     content.includes("localhost:20128") ||
+    (!!port && content.includes(`localhost:${port}`)) ||
     content.includes("OMNIROUTE_BASE_URL")
   );
 }
@@ -170,7 +172,9 @@ export async function detectTool(id: string): Promise {
       : getCliPrimaryConfigPath(tool.id) ||
         (tool.id === "opencode" ? resolveOpencodeConfigPath() : "");
   const configContents = await readConfigFile(configPath);
-  const configured = !!configContents && isConfigured(configContents, "http://localhost:20128");
+  const defaultPort = process.env.PORT || process.env.DASHBOARD_PORT || 20128;
+  const configured =
+    !!configContents && isConfigured(configContents, `http://localhost:${defaultPort}`);
 
   const result: DetectedTool = {
     id: canonicalId,
@@ -187,12 +191,14 @@ export async function detectTool(id: string): Promise {
     try {
       const roles = await getCurrentHermesAgentRoles();
       const richRoles: Record = {};
+      const currentPort = String(process.env.PORT || process.env.DASHBOARD_PORT || 20128);
 
       Object.entries(roles).forEach(([role, info]) => {
         const usingOmni =
           info?.provider === "omniroute" ||
           (info?.base_url || "").includes("20128") ||
-          (info?.base_url || "").includes("localhost:20128");
+          (info?.base_url || "").includes(currentPort) ||
+          (info?.base_url || "").includes("localhost");
 
         richRoles[role] = {
           model: info.model,
diff --git a/src/lib/wellKnown.ts b/src/lib/wellKnown.ts
index 7a6f2a35bf..341d639888 100644
--- a/src/lib/wellKnown.ts
+++ b/src/lib/wellKnown.ts
@@ -10,5 +10,6 @@ export function getBaseUrl(request?: NextRequest | null): string {
   if (process.env.OMNIROUTE_BASE_URL) return process.env.OMNIROUTE_BASE_URL;
   // Direct route-handler invocation (unit tests, programmatic calls) passes no
   // Request — fall back to the default local gateway origin instead of crashing.
-  return request?.nextUrl?.origin ?? "http://localhost:20128";
+  const defaultPort = process.env.PORT || process.env.DASHBOARD_PORT || 20128;
+  return request?.nextUrl?.origin ?? `http://localhost:${defaultPort}`;
 }
diff --git a/src/mitm/handlers/base.ts b/src/mitm/handlers/base.ts
index 18b4a86dfe..943e1c38b6 100644
--- a/src/mitm/handlers/base.ts
+++ b/src/mitm/handlers/base.ts
@@ -179,7 +179,9 @@ export abstract class MitmHandlerBase {
     path: string,
     headers: IncomingHttpHeaders,
   ): Promise {
-    const base = process.env.OMNIROUTE_BASE_URL ?? "http://127.0.0.1:20128";
+    const port = process.env.API_PORT || process.env.PORT || 20128;
+    const base =
+      process.env.OMNIROUTE_BASE_URL ?? process.env.BASE_URL ?? `http://127.0.0.1:${port}`;
     const url = `${base.replace(/\/+$/, "")}${path}`;
     const apiKey = process.env.ROUTER_API_KEY ?? "";
 
diff --git a/src/mitm/server.cjs b/src/mitm/server.cjs
index da0d6bba3e..7a6a61cb82 100644
--- a/src/mitm/server.cjs
+++ b/src/mitm/server.cjs
@@ -42,7 +42,7 @@ const MITM_IDLE_TIMEOUT_MS =
 const ROUTER_BASE_URL = (
   process.env.OMNIROUTE_BASE_URL ||
   process.env.BASE_URL ||
-  "http://localhost:20128"
+  `http://localhost:${process.env.API_PORT || process.env.PORT || 20128}`
 )
   .trim()
   .replace(/\/+$/, "");
diff --git a/src/server/origin/publicOrigin.ts b/src/server/origin/publicOrigin.ts
index c925b8e166..9cc6e78f21 100644
--- a/src/server/origin/publicOrigin.ts
+++ b/src/server/origin/publicOrigin.ts
@@ -3,10 +3,7 @@ import { PEER_IP_HEADER } from "@/server/authz/headers";
 import { resolveStampedPeer } from "@/server/authz/peerStamp";
 
 export type PublicOriginSource =
-  | "configured"
-  | "trusted-forwarded"
-  | "request-url"
-  | "direct-local-host";
+  "configured" | "trusted-forwarded" | "request-url" | "direct-local-host";
 
 export interface PublicOriginCandidate {
   origin: string;
@@ -200,7 +197,7 @@ function directLocalHostOrigin(request: Request): string | null {
   if (classifyHostLocality(peer) === "remote") return null;
 
   const rawHost = trustsForwardedHeaders(request)
-    ? firstHeaderValue(request.headers.get("x-forwarded-host")) ?? request.headers.get("host")
+    ? (firstHeaderValue(request.headers.get("x-forwarded-host")) ?? request.headers.get("host"))
     : request.headers.get("host");
   const host = sanitizeForwardedHost(rawHost);
   if (!host) return null;
@@ -246,7 +243,8 @@ export function resolvePublicOrigin(request: Request): PublicOriginCandidate {
   const requestOrigin = requestUrlOrigin(request);
   if (requestOrigin) return { origin: requestOrigin, source: "request-url" };
 
-  return { origin: "http://localhost:20128", source: "request-url" };
+  const defaultPort = process.env.PORT || process.env.DASHBOARD_PORT || "20128";
+  return { origin: `http://localhost:${defaultPort}`, source: "request-url" };
 }
 
 export function validateBrowserMutationOrigin(request: Request): BrowserMutationOriginVerdict {
diff --git a/src/server/ws/liveServerAllowList.ts b/src/server/ws/liveServerAllowList.ts
index 1f3101f054..c11e09550b 100644
--- a/src/server/ws/liveServerAllowList.ts
+++ b/src/server/ws/liveServerAllowList.ts
@@ -45,7 +45,17 @@ export function parseCsvEnv(value: string | undefined | null): Set {
  */
 export function buildAllowedOrigins(env: NodeJS.ProcessEnv = process.env): Set {
   const extra = parseCsvEnv(env.LIVE_WS_ALLOWED_ORIGINS);
-  return new Set([...DEFAULT_ALLOWED_ORIGINS, ...extra]);
+  const runtimePort = env.PORT || env.DASHBOARD_PORT;
+  const dynamicDefaults: string[] = [];
+  if (runtimePort && runtimePort !== "20128") {
+    dynamicDefaults.push(
+      `http://127.0.0.1:${runtimePort}`,
+      `http://localhost:${runtimePort}`,
+      `http://[::1]:${runtimePort}`,
+      `http://0.0.0.0:${runtimePort}`
+    );
+  }
+  return new Set([...DEFAULT_ALLOWED_ORIGINS, ...dynamicDefaults, ...extra]);
 }
 
 /**
diff --git a/src/shared/hooks/useDisplayBaseUrl.ts b/src/shared/hooks/useDisplayBaseUrl.ts
index c58e2344f9..3984aa487e 100644
--- a/src/shared/hooks/useDisplayBaseUrl.ts
+++ b/src/shared/hooks/useDisplayBaseUrl.ts
@@ -209,7 +209,11 @@ export function resolveDisplayBaseUrl(
     return joinOriginAndBasePath(configuredUrl, basePath);
   }
 
-  const fallback = currentOrigin ?? configuredUrl ?? DEFAULT_DISPLAY_BASE_URL;
+  const portFallback =
+    typeof process !== "undefined" && (process.env.NEXT_PUBLIC_PORT || process.env.PORT)
+      ? `http://localhost:${process.env.NEXT_PUBLIC_PORT || process.env.PORT}`
+      : DEFAULT_DISPLAY_BASE_URL;
+  const fallback = currentOrigin ?? configuredUrl ?? portFallback;
   return joinOriginAndBasePath(fallback, basePath);
 }
 
diff --git a/src/shared/utils/resolveOmniRouteBaseUrl.ts b/src/shared/utils/resolveOmniRouteBaseUrl.ts
index 3f4c18f33b..45fa245792 100644
--- a/src/shared/utils/resolveOmniRouteBaseUrl.ts
+++ b/src/shared/utils/resolveOmniRouteBaseUrl.ts
@@ -4,6 +4,9 @@ type OmniRouteBaseUrlEnv = {
   OMNIROUTE_BASE_URL?: string;
   BASE_URL?: string;
   NEXT_PUBLIC_BASE_URL?: string;
+  PORT?: string | number;
+  API_PORT?: string | number;
+  DASHBOARD_PORT?: string | number;
 };
 
 function normalizeBaseUrl(value?: string): string | null {
@@ -13,11 +16,14 @@ function normalizeBaseUrl(value?: string): string | null {
 }
 
 export function resolveOmniRouteBaseUrl(env: OmniRouteBaseUrlEnv = process.env): string {
+  const port = env.PORT || env.API_PORT || env.DASHBOARD_PORT;
+  const fallback = port ? `http://localhost:${port}` : DEFAULT_OMNIROUTE_BASE_URL;
+
   return (
     normalizeBaseUrl(env.OMNIROUTE_BASE_URL) ||
     normalizeBaseUrl(env.BASE_URL) ||
     normalizeBaseUrl(env.NEXT_PUBLIC_BASE_URL) ||
-    DEFAULT_OMNIROUTE_BASE_URL
+    fallback
   );
 }
 
diff --git a/tests/unit/resolve-omniroute-base-url.test.ts b/tests/unit/resolve-omniroute-base-url.test.ts
index 3e629fe1e6..a8a76e9889 100644
--- a/tests/unit/resolve-omniroute-base-url.test.ts
+++ b/tests/unit/resolve-omniroute-base-url.test.ts
@@ -50,3 +50,7 @@ test("resolveOmniRouteBaseUrl ignores blank values", () => {
 test("resolveOmniRouteBaseUrl uses the default localhost fallback", () => {
   assert.equal(resolveOmniRouteBaseUrl({}), DEFAULT_OMNIROUTE_BASE_URL);
 });
+
+test("resolveOmniRouteBaseUrl uses custom port when PORT env is set", () => {
+  assert.equal(resolveOmniRouteBaseUrl({ PORT: 37128 }), "http://localhost:37128");
+});
diff --git a/tests/unit/security/live-server-allowlist.test.ts b/tests/unit/security/live-server-allowlist.test.ts
index fbd4fd9be8..041c784797 100644
--- a/tests/unit/security/live-server-allowlist.test.ts
+++ b/tests/unit/security/live-server-allowlist.test.ts
@@ -66,6 +66,18 @@ describe("buildAllowedOrigins", () => {
     // Defaults remain.
     assert.equal(out.has("http://localhost:20128"), true);
   });
+
+  it("includes dynamic loopback origins when custom PORT is configured", () => {
+    const env = {
+      ...EMPTY_ENV,
+      PORT: "37128",
+    };
+    const out = buildAllowedOrigins(env);
+    assert.equal(out.has("http://localhost:37128"), true);
+    assert.equal(out.has("http://127.0.0.1:37128"), true);
+    assert.equal(out.has("http://[::1]:37128"), true);
+    assert.equal(out.has("http://localhost:20128"), true);
+  });
 });
 
 describe("buildAllowedHosts", () => {
@@ -150,6 +162,11 @@ describe("isOriginAllowed", () => {
     assert.equal(isOriginAllowed("http://100.96.135.160:20128", env), true);
   });
 
+  it("does not treat a wildcard host as an allow-all origin policy", () => {
+    const env = { ...EMPTY_ENV, LIVE_WS_ALLOWED_HOSTS: "*" };
+    assert.equal(isOriginAllowed("http://100.90.139.116:37128", env), false);
+  });
+
   it("does NOT accept a Tailscale Origin when LIVE_WS_ALLOWED_HOSTS is unset", () => {
     // Critical security invariant: without explicit opt-in, the LAN/Tailscale
     // surface is closed even though the listener is reachable.