From 676cf37ee21648f57c1cf0f558d7bcc052c785b4 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 4 Jun 2026 20:43:37 -0700 Subject: [PATCH] Make sure things are paywalled in the blueprints --- server/lib/blueprints/clientResources.ts | 34 ++++++++- server/lib/blueprints/proxyResources.ts | 92 ++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 2 deletions(-) diff --git a/server/lib/blueprints/clientResources.ts b/server/lib/blueprints/clientResources.ts index 44cd9956b..34e668984 100644 --- a/server/lib/blueprints/clientResources.ts +++ b/server/lib/blueprints/clientResources.ts @@ -23,6 +23,8 @@ import logger from "@server/logger"; import { defaultRoleAllowedActions } from "@server/routers/role/createRole"; import { getNextAvailableAliasAddress } from "../ip"; import { createCertificate } from "#dynamic/routers/certificates/createCertificate"; +import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; +import { tierMatrix } from "../billing/tierMatrix"; async function getDomainForSiteResource( siteResourceId: number | undefined, @@ -114,6 +116,30 @@ export async function updateClientResources( for (const [resourceNiceId, resourceData] of Object.entries( config["client-resources"] )) { + if (resourceData.mode === "http") { + const hasHttpFeature = await isLicensedOrSubscribed( + orgId, + tierMatrix.advancedPrivateResources + ); + if (!hasHttpFeature) { + throw new Error( + "HTTP private resources are not included in your current plan. Please upgrade." + ); + } + } + + if (resourceData.mode === "ssh") { + const hasSshFeature = await isLicensedOrSubscribed( + orgId, + tierMatrix.advancedPrivateResources + ); + if (!hasSshFeature) { + throw new Error( + "SSH private resources are not included in your current plan. Please upgrade." + ); + } + } + const [existingResource] = await trx .select() .from(siteResources) @@ -366,7 +392,9 @@ export async function updateClientResources( })) ); existingRoles.push(created); - logger.info(`Auto-created role "${name}" in org ${orgId} from blueprint`); + logger.info( + `Auto-created role "${name}" in org ${orgId} from blueprint` + ); } const roleIds = existingRoles.map((role) => role.roleId); @@ -510,7 +538,9 @@ export async function updateClientResources( })) ); existingRoles.push(created); - logger.info(`Auto-created role "${name}" in org ${orgId} from blueprint`); + logger.info( + `Auto-created role "${name}" in org ${orgId} from blueprint` + ); } const roleIds = existingRoles.map((role) => role.roleId); diff --git a/server/lib/blueprints/proxyResources.ts b/server/lib/blueprints/proxyResources.ts index 28bc1c90d..6c37d17b8 100644 --- a/server/lib/blueprints/proxyResources.ts +++ b/server/lib/blueprints/proxyResources.ts @@ -47,6 +47,7 @@ import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; import { fireHealthCheckUnknownAlert } from "@server/lib/alerts"; import { tierMatrix } from "../billing/tierMatrix"; import { defaultRoleAllowedActions } from "@server/routers/role/createRole"; +import { build } from "@server/build"; export type ProxyResourcesResults = { proxyResource: Resource; @@ -222,17 +223,59 @@ export async function updateProxyResources( headers = JSON.stringify(resourceData.headers); } + if (["ssh", "rdp", "vnc"].includes(resourceData.mode || "")) { + const isLicensed = await isLicensedOrSubscribed( + orgId, + tierMatrix.advancedPublicResources + ); + if (!isLicensed) { + throw new Error( + "Your current subscription does not support browser gateway resources. Please upgrade to access this feature." + ); + } + } + + if (resourceData.policy) { + const isLicensed = await isLicensedOrSubscribed( + orgId, + tierMatrix.resourcePolicies + ); + if (!isLicensed) { + throw new Error( + "Your current subscription does not support shared resource policies. Please upgrade to access this feature." + ); + } + } + if (existingResource) { let domain; if ( ["http", "ssh", "rdp", "vnc"].includes(resourceData.mode || "") ) { + if (resourceData["full-domain"]?.startsWith("*.")) { + const isLicensed = await isLicensedOrSubscribed( + orgId, + tierMatrix.wildcardSubdomain + ); + if (!isLicensed) { + throw new Error( + "Wildcard subdomains are not supported on your current plan. Please upgrade to access this feature." + ); + } + } + domain = await getDomain( existingResource.resourceId, resourceData["full-domain"]!, orgId, trx ); + + await enforceDomainNamespacePaywall( + orgId, + domain.domainId, + trx + ); } // check if the only key in the resource is targets, if so, skip the update @@ -906,12 +949,30 @@ export async function updateProxyResources( if ( ["http", "ssh", "rdp", "vnc"].includes(resourceData.mode || "") ) { + if (resourceData["full-domain"]?.startsWith("*.")) { + const isLicensed = await isLicensedOrSubscribed( + orgId, + tierMatrix.wildcardSubdomain + ); + if (!isLicensed) { + throw new Error( + "Wildcard subdomains are not supported on your current plan. Please upgrade to access this feature." + ); + } + } + domain = await getDomain( undefined, resourceData["full-domain"]!, orgId, trx ); + + await enforceDomainNamespacePaywall( + orgId, + domain.domainId, + trx + ); } const isLicensed = await isLicensedOrSubscribed( @@ -1866,6 +1927,37 @@ function checkIfTargetChanged( return false; } +async function enforceDomainNamespacePaywall( + orgId: string, + domainId: string, + trx: Transaction +) { + if (build !== "saas") { + return; + } + + const hasDomainNamespaceAccess = await isLicensedOrSubscribed( + orgId, + tierMatrix.domainNamespaces + ); + + if (hasDomainNamespaceAccess) { + return; + } + + const [namespaceDomain] = await trx + .select() + .from(domainNamespaces) + .where(eq(domainNamespaces.domainId, domainId)) + .limit(1); + + if (namespaceDomain) { + throw new Error( + "Your current subscription does not support custom domain namespaces. Please upgrade to access this feature." + ); + } +} + export async function getDomain( resourceId: number | undefined, fullDomain: string,