From 4989d1e31a249d28816e3e0d9b8683edf3ff647f Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 14 Aug 2026 11:31:56 -0400 Subject: [PATCH] Allow overlapping domains on public inference resources --- server/lib/blueprints/publicResources.ts | 11 +++++++++++ server/routers/resource/createResource.ts | 17 ++++++++++++++--- server/routers/resource/updateResource.ts | 15 ++++++++++++++- .../siteResource/listAllSiteResourcesByOrg.ts | 4 ++-- 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/server/lib/blueprints/publicResources.ts b/server/lib/blueprints/publicResources.ts index 9c9242984..806d97e9c 100644 --- a/server/lib/blueprints/publicResources.ts +++ b/server/lib/blueprints/publicResources.ts @@ -306,6 +306,7 @@ export async function updatePublicResources( existingResource.resourceId, resourceData["full-domain"]!, orgId, + resourceData.mode === "inference", trx ); @@ -1098,6 +1099,7 @@ export async function updatePublicResources( undefined, resourceData["full-domain"]!, orgId, + resourceData.mode === "inference", trx ); @@ -2113,6 +2115,7 @@ export async function getDomain( resourceId: number | undefined, fullDomain: string, orgId: string, + isInference: boolean, trx: Transaction ) { const [fullDomainExists] = await trx @@ -2122,6 +2125,14 @@ export async function getDomain( and( eq(resources.fullDomain, fullDomain), eq(resources.orgId, orgId), + // Inference resources route through the central AI gateway + // rather than normal target-based proxying, so they're + // allowed to share a full-domain with a non-inference + // resource (and vice versa) - only conflicts within the + // same routing category are rejected. + isInference + ? eq(resources.mode, "inference") + : ne(resources.mode, "inference"), resourceId ? ne(resources.resourceId, resourceId) : isNotNull(resources.resourceId) diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index 45e66c2c3..897d32f6a 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -18,7 +18,7 @@ import { import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; -import { eq, and } from "drizzle-orm"; +import { eq, and, ne } from "drizzle-orm"; import { fromError } from "zod-validation-error"; import logger from "@server/logger"; import { subdomainSchema, wildcardSubdomainSchema } from "@server/lib/schemas"; @@ -484,11 +484,22 @@ async function createHttpResource( logger.debug(`Full domain: ${fullDomain}`); - // make sure the full domain is unique + // make sure the full domain is unique. Inference resources are routed + // through the central AI gateway rather than normal target-based + // proxying, so they're allowed to share a full-domain with a + // non-inference resource (and vice versa) - only conflicts within the + // same routing category are rejected. const existingResource = await db .select() .from(resources) - .where(eq(resources.fullDomain, fullDomain)); + .where( + and( + eq(resources.fullDomain, fullDomain), + effectiveMode === "inference" + ? eq(resources.mode, "inference") + : ne(resources.mode, "inference") + ) + ); if (existingResource.length > 0) { return next( diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index 0d6f8f8aa..3ed26c9cb 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -596,10 +596,23 @@ async function updateHttpResource( logger.debug(`Full domain: ${fullDomain}`); if (fullDomain) { + // Inference resources route through the central AI gateway + // rather than normal target-based proxying, so they're allowed + // to share a full-domain with a non-inference resource (and + // vice versa) - only conflicts within the same routing category + // are rejected. mode isn't updatable here, so `resource.mode` + // reflects the resource's actual (unchanging) routing category. const [existingDomain] = await db .select() .from(resources) - .where(eq(resources.fullDomain, fullDomain)); + .where( + and( + eq(resources.fullDomain, fullDomain), + resource.mode === "inference" + ? eq(resources.mode, "inference") + : ne(resources.mode, "inference") + ) + ); if ( existingDomain && diff --git a/server/routers/siteResource/listAllSiteResourcesByOrg.ts b/server/routers/siteResource/listAllSiteResourcesByOrg.ts index 50bbca6b4..7eb54c829 100644 --- a/server/routers/siteResource/listAllSiteResourcesByOrg.ts +++ b/server/routers/siteResource/listAllSiteResourcesByOrg.ts @@ -55,12 +55,12 @@ const listAllSiteResourcesByOrgQuerySchema = z.strictObject({ }), query: z.string().optional(), mode: z - .enum(["host", "cidr", "http"]) + .enum(["host", "cidr", "http", "ssh", "inference"]) .optional() .catch(undefined) .openapi({ type: "string", - enum: ["host", "cidr", "http"], + enum: ["host", "cidr", "http", "ssh", "inference"], description: "Filter site resources by mode" }), sort_by: z