show exit nodes in resource launcher

This commit is contained in:
miloschwartz
2026-09-29 17:05:07 -04:00
parent c0c0cfb18a
commit a46fc531d4
9 changed files with 64 additions and 10 deletions
+3
View File
@@ -4314,9 +4314,12 @@
"resourceLauncherAuthMethodsDescription": "Authentication methods enabled for this resource.",
"resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.",
"resourceLauncherPrivateClientRequiredTitle": "Client Connection Required",
"resourceLauncherExitNodeTitle": "Exit Node Routing",
"resourceLauncherExitNodeDescription": "After connecting your client, pick this exit node in the client menu to route all traffic through the attached sites.",
"resourceLauncherDownloadClient": "Download client",
"resourceLauncherFailedToLoadDetails": "Could not load resource details. You may no longer have access to this resource.",
"resourceLauncherNoPortRestrictions": "No port restrictions",
"resourceLauncherAllInternetTraffic": "All internet traffic",
"resourceLauncherTcp": "TCP",
"resourceLauncherUdp": "UDP",
"resourceLauncherUnlabeled": "Unlabeled",
@@ -125,6 +125,16 @@ export function formatPublicResourceAccess(
export function formatSiteResourceAccess(
resource: SiteResourceAccessInput
): LauncherAccessFields {
// Exit node (gateway) destinations are always 0.0.0.0/0 — not useful to
// show or copy. The launcher UI renders a localized capability label.
if (resource.mode === "gateway") {
return {
accessDisplay: "",
accessCopyValue: "",
accessUrl: null
};
}
if (
(resource.mode === "http" || resource.mode === "inference") &&
resource.fullDomain
@@ -55,12 +55,12 @@ const listAllSiteResourcesByOrgQuerySchema = z.strictObject({
}),
query: z.string().optional(),
mode: z
.enum(["host", "cidr", "http", "ssh", "inference"])
.enum(["host", "cidr", "http", "ssh", "inference", "gateway"])
.optional()
.catch(undefined)
.openapi({
type: "string",
enum: ["host", "cidr", "http", "ssh", "inference"],
enum: ["host", "cidr", "http", "ssh", "inference", "gateway"],
description: "Filter site resources by mode"
}),
sort_by: z
+6
View File
@@ -357,6 +357,12 @@ export default function PrivateResourcesTable({
label: t(
"editInternalResourceDialogModeInference"
)
},
{
value: "gateway",
label: t(
"editInternalResourceDialogModeGateway"
)
}
]}
selectedValue={searchParams.get("mode") ?? undefined}
@@ -1,6 +1,7 @@
"use client";
import { isSafeUrlForLink } from "@app/lib/launcherResourceAccess";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { LauncherCopyIcon } from "./LauncherCopyIcon";
@@ -8,6 +9,7 @@ type LauncherResourceAccessProps = {
accessDisplay: string;
accessCopyValue: string;
accessUrl?: string | null;
mode?: string;
variant: "grid" | "list";
};
@@ -15,15 +17,23 @@ export function LauncherResourceAccess({
accessDisplay,
accessCopyValue,
accessUrl,
mode,
variant
}: LauncherResourceAccessProps) {
if (!accessDisplay) {
const t = useTranslations();
const isExitNode = mode === "gateway";
const display = isExitNode
? t("resourceLauncherAllInternetTraffic")
: accessDisplay;
if (!display) {
return null;
}
const href = accessUrl ?? undefined;
const canLink = href && isSafeUrlForLink(href);
const canLink = !isExitNode && href && isSafeUrlForLink(href);
const copyValue = canLink ? href : accessCopyValue;
const showCopy = !isExitNode && Boolean(copyValue);
if (variant === "list") {
return (
@@ -35,14 +45,14 @@ export function LauncherResourceAccess({
rel="noopener noreferrer"
className="min-w-0 truncate text-sm text-muted-foreground hover:underline max-md:overflow-visible max-md:whitespace-nowrap"
>
{accessDisplay}
{display}
</Link>
) : (
<span className="min-w-0 truncate text-sm text-muted-foreground max-md:overflow-visible max-md:whitespace-nowrap">
{accessDisplay}
{display}
</span>
)}
<LauncherCopyIcon text={copyValue} />
{showCopy ? <LauncherCopyIcon text={copyValue} /> : null}
</div>
);
}
@@ -56,14 +66,14 @@ export function LauncherResourceAccess({
rel="noopener noreferrer"
className="min-w-0 flex-1 truncate text-sm text-muted-foreground hover:underline"
>
{accessDisplay}
{display}
</Link>
) : (
<span className="min-w-0 flex-1 truncate text-sm text-muted-foreground">
{accessDisplay}
{display}
</span>
)}
<LauncherCopyIcon text={copyValue} />
{showCopy ? <LauncherCopyIcon text={copyValue} /> : null}
</div>
);
}
@@ -56,6 +56,7 @@ export function LauncherResourceCard({
accessDisplay={resource.accessDisplay}
accessCopyValue={resource.accessCopyValue}
accessUrl={resource.accessUrl}
mode={resource.mode}
variant="grid"
/>
</div>
@@ -654,6 +654,7 @@ function PrivateResourceDetails({
const t = useTranslations();
const isInference = resource.mode === "inference";
const isSsh = resource.mode === "ssh";
const isExitNode = resource.mode === "gateway";
return (
<div className="space-y-4">
@@ -678,6 +679,18 @@ function PrivateResourceDetails({
</AlertDescription>
</Alert>
{isExitNode ? (
<Alert variant="default">
<AlertCircle className="size-4" />
<AlertTitle>
{t("resourceLauncherExitNodeTitle")}
</AlertTitle>
<AlertDescription>
{t("resourceLauncherExitNodeDescription")}
</AlertDescription>
</Alert>
) : null}
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
@@ -51,6 +51,7 @@ export function LauncherResourceRow({
accessDisplay={resource.accessDisplay}
accessCopyValue={resource.accessCopyValue}
accessUrl={resource.accessUrl}
mode={resource.mode}
variant="list"
/>
+10
View File
@@ -66,6 +66,16 @@ export function formatPublicResourceAccess(
export function formatSiteResourceAccess(
resource: SiteResourceAccessInput
): LauncherAccessFields {
// Exit node (gateway) destinations are always 0.0.0.0/0 — not useful to
// show or copy. Callers should render a localized capability label.
if (resource.mode === "gateway") {
return {
accessDisplay: "",
accessCopyValue: "",
accessUrl: null
};
}
if (
(resource.mode === "http" || resource.mode === "inference") &&
resource.fullDomain