From 81eba50c9a3a6b5353e4baa58ba2216ccbd7401b Mon Sep 17 00:00:00 2001
From: Laurence
Date: Mon, 6 Apr 2026 14:03:33 +0100
Subject: [PATCH 1/8] fix: use targetId as row identifier
fix: 2797
---
src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx
index 3d6e6186b..a9128b9d3 100644
--- a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx
+++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx
@@ -678,6 +678,7 @@ function ProxyResourceTargetsForm({
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
+ getRowId: (row) => String(row.targetId),
state: {
pagination: {
pageIndex: 0,
From 7d3d5b2b22aafa4e0b6bc5584ec65ba405c80878 Mon Sep 17 00:00:00 2001
From: Laurence
Date: Mon, 6 Apr 2026 14:17:04 +0100
Subject: [PATCH 2/8] use targetid also on proxy create as that also has same
issue
---
src/app/[orgId]/settings/resources/proxy/create/page.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/app/[orgId]/settings/resources/proxy/create/page.tsx b/src/app/[orgId]/settings/resources/proxy/create/page.tsx
index f057c07c4..f5c20d8cc 100644
--- a/src/app/[orgId]/settings/resources/proxy/create/page.tsx
+++ b/src/app/[orgId]/settings/resources/proxy/create/page.tsx
@@ -999,6 +999,7 @@ export default function Page() {
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
+ getRowId: (row) => String(row.targetId),
state: {
pagination: {
pageIndex: 0,
From 028df8bf27a8c5d8879ce931974f7787acb3083e Mon Sep 17 00:00:00 2001
From: Joshua Belke
Date: Tue, 7 Apr 2026 14:58:27 -0400
Subject: [PATCH 3/8] fix: remove encodeURIComponent from invite link email
parameter
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The @ symbol in email addresses was being encoded as %40 when
constructing invite URLs, causing broken or garbled links when
copied/shared by users.
- Remove encodeURIComponent(email) from server-side invite link
construction in inviteUser.ts (both new invite and regenerate paths)
- Remove encodeURIComponent(email) from client-side redirect URLs in
InviteStatusCard.tsx (login, signup, and useEffect redirect paths)
- Valid Zod-validated email addresses do not contain characters that
require URL encoding for safe query parameter use (@ is permitted
in query strings per RFC 3986 §3.4)
---
server/routers/user/inviteUser.ts | 22 ++++++++++++++--------
src/components/InviteStatusCard.tsx | 18 ++++++++++++------
2 files changed, 26 insertions(+), 14 deletions(-)
diff --git a/server/routers/user/inviteUser.ts b/server/routers/user/inviteUser.ts
index 7ac1849b9..b11586e69 100644
--- a/server/routers/user/inviteUser.ts
+++ b/server/routers/user/inviteUser.ts
@@ -1,7 +1,14 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
-import { orgs, roles, userInviteRoles, userInvites, userOrgs, users } from "@server/db";
+import {
+ orgs,
+ roles,
+ userInviteRoles,
+ userInvites,
+ userOrgs,
+ users
+} from "@server/db";
import { and, eq, inArray } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
@@ -37,8 +44,7 @@ const inviteUserBodySchema = z
regenerate: z.boolean().optional()
})
.refine(
- (d) =>
- (d.roleIds != null && d.roleIds.length > 0) || d.roleId != null,
+ (d) => (d.roleIds != null && d.roleIds.length > 0) || d.roleId != null,
{ message: "roleIds or roleId is required", path: ["roleIds"] }
)
.transform((data) => ({
@@ -265,7 +271,7 @@ export async function inviteUser(
)
);
- const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${encodeURIComponent(email)}`;
+ const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${email}`;
if (doEmail) {
await sendEmail(
@@ -314,12 +320,12 @@ export async function inviteUser(
expiresAt,
tokenHash
});
- await trx.insert(userInviteRoles).values(
- uniqueRoleIds.map((roleId) => ({ inviteId, roleId }))
- );
+ await trx
+ .insert(userInviteRoles)
+ .values(uniqueRoleIds.map((roleId) => ({ inviteId, roleId })));
});
- const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${encodeURIComponent(email)}`;
+ const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${email}`;
if (doEmail) {
await sendEmail(
diff --git a/src/components/InviteStatusCard.tsx b/src/components/InviteStatusCard.tsx
index 417fa9892..5de8f25fd 100644
--- a/src/components/InviteStatusCard.tsx
+++ b/src/components/InviteStatusCard.tsx
@@ -39,7 +39,11 @@ export default function InviteStatusCard({
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [type, setType] = useState<
- "rejected" | "wrong_user" | "user_does_not_exist" | "not_logged_in" | "user_limit_exceeded"
+ | "rejected"
+ | "wrong_user"
+ | "user_does_not_exist"
+ | "not_logged_in"
+ | "user_limit_exceeded"
>("rejected");
useEffect(() => {
@@ -90,12 +94,12 @@ export default function InviteStatusCard({
if (!user && type === "user_does_not_exist") {
const redirectUrl = email
- ? `/auth/signup?redirect=/invite?token=${tokenParam}&email=${encodeURIComponent(email)}`
+ ? `/auth/signup?redirect=/invite?token=${tokenParam}&email=${email}`
: `/auth/signup?redirect=/invite?token=${tokenParam}`;
router.push(redirectUrl);
} else if (!user && type === "not_logged_in") {
const redirectUrl = email
- ? `/auth/login?redirect=/invite?token=${tokenParam}&email=${encodeURIComponent(email)}`
+ ? `/auth/login?redirect=/invite?token=${tokenParam}&email=${email}`
: `/auth/login?redirect=/invite?token=${tokenParam}`;
router.push(redirectUrl);
} else {
@@ -109,7 +113,7 @@ export default function InviteStatusCard({
async function goToLogin() {
await api.post("/auth/logout", {});
const redirectUrl = email
- ? `/auth/login?redirect=/invite?token=${tokenParam}&email=${encodeURIComponent(email)}`
+ ? `/auth/login?redirect=/invite?token=${tokenParam}&email=${email}`
: `/auth/login?redirect=/invite?token=${tokenParam}`;
router.push(redirectUrl);
}
@@ -117,7 +121,7 @@ export default function InviteStatusCard({
async function goToSignup() {
await api.post("/auth/logout", {});
const redirectUrl = email
- ? `/auth/signup?redirect=/invite?token=${tokenParam}&email=${encodeURIComponent(email)}`
+ ? `/auth/signup?redirect=/invite?token=${tokenParam}&email=${email}`
: `/auth/signup?redirect=/invite?token=${tokenParam}`;
router.push(redirectUrl);
}
@@ -157,7 +161,9 @@ export default function InviteStatusCard({
Cannot Accept Invite
- This organization has reached its user limit. Please contact the organization administrator to upgrade their plan before accepting this invite.
+ This organization has reached its user limit. Please
+ contact the organization administrator to upgrade their
+ plan before accepting this invite.
);
From 840684aebab3289a0c4b04b6a48b9747cd05aa79 Mon Sep 17 00:00:00 2001
From: miloschwartz
Date: Thu, 9 Apr 2026 17:54:08 -0400
Subject: [PATCH 4/8] dont show wildcard in domain picker
---
messages/en-US.json | 1 +
src/components/DomainPicker.tsx | 29 ++++++++++++++++++++---------
2 files changed, 21 insertions(+), 9 deletions(-)
diff --git a/messages/en-US.json b/messages/en-US.json
index 7642419c6..9fa4e730d 100644
--- a/messages/en-US.json
+++ b/messages/en-US.json
@@ -2116,6 +2116,7 @@
"domainPickerFreeProvidedDomain": "Free Provided Domain",
"domainPickerVerified": "Verified",
"domainPickerUnverified": "Unverified",
+ "domainPickerManual": "Manual",
"domainPickerInvalidSubdomainStructure": "This subdomain contains invalid characters or structure. It will be sanitized automatically when you save.",
"domainPickerError": "Error",
"domainPickerErrorLoadDomains": "Failed to load organization domains",
diff --git a/src/components/DomainPicker.tsx b/src/components/DomainPicker.tsx
index 44446763b..afb273b5c 100644
--- a/src/components/DomainPicker.tsx
+++ b/src/components/DomainPicker.tsx
@@ -509,9 +509,11 @@ export default function DomainPicker({
{selectedBaseDomain.domain}
- {selectedBaseDomain.verified && (
-
- )}
+ {selectedBaseDomain.verified &&
+ selectedBaseDomain.domainType !==
+ "wildcard" && (
+
+ )}
) : (
t("domainPickerSelectBaseDomain")
@@ -574,14 +576,23 @@ export default function DomainPicker({
}
- {orgDomain.type.toUpperCase()}{" "}
- •{" "}
- {orgDomain.verified
+ {orgDomain.type ===
+ "wildcard"
? t(
- "domainPickerVerified"
+ "domainPickerManual"
)
- : t(
- "domainPickerUnverified"
+ : (
+ <>
+ {orgDomain.type.toUpperCase()}{" "}
+ •{" "}
+ {orgDomain.verified
+ ? t(
+ "domainPickerVerified"
+ )
+ : t(
+ "domainPickerUnverified"
+ )}
+ >
)}
From 1aedf9da0ac258e58735da61a26ab1dc00aa08d8 Mon Sep 17 00:00:00 2001
From: Adnan Silajdzic
Date: Fri, 10 Apr 2026 13:37:15 +0000
Subject: [PATCH 5/8] fix(worldmap): avoid stuck country hover state
---
src/components/WorldMap.tsx | 24 +++++++++++++++++-------
1 file changed, 17 insertions(+), 7 deletions(-)
diff --git a/src/components/WorldMap.tsx b/src/components/WorldMap.tsx
index c64c3f430..9dbf6de80 100644
--- a/src/components/WorldMap.tsx
+++ b/src/components/WorldMap.tsx
@@ -164,7 +164,7 @@ const countryClass = cn(
const highlightedCountryClass = cn(
sharedCountryClass,
- "stroke-2",
+ "stroke-[3]",
"fill-[#f4f4f5]",
"stroke-[#f36117]",
"dark:fill-[#3f3f46]"
@@ -194,11 +194,20 @@ function drawInteractiveCountries(
const path = setupProjetionPath();
const data = parseWorldTopoJsonToGeoJsonFeatures();
const svg = d3.select(element);
+ const countriesLayer = svg.append("g");
+ const hoverLayer = svg.append("g").style("pointer-events", "none");
+ const hoverPath = hoverLayer
+ .append("path")
+ .datum(null)
+ .attr("class", highlightedCountryClass)
+ .style("display", "none");
- svg.selectAll("path")
+ countriesLayer
+ .selectAll("path")
.data(data)
.enter()
.append("path")
+ .attr("data-country-path", "true")
.attr("class", countryClass)
.attr("d", path as never)
@@ -209,9 +218,10 @@ function drawInteractiveCountries(
y,
hoveredCountryAlpha3Code: country.properties.a3
});
- // brings country to front
- this.parentNode?.appendChild(this);
- d3.select(this).attr("class", highlightedCountryClass);
+ hoverPath
+ .datum(country)
+ .attr("d", path(country) as string)
+ .style("display", null);
})
.on("mousemove", function (event) {
@@ -221,7 +231,7 @@ function drawInteractiveCountries(
.on("mouseout", function () {
setTooltip({ x: 0, y: 0, hoveredCountryAlpha3Code: null });
- d3.select(this).attr("class", countryClass);
+ hoverPath.style("display", "none");
});
return svg;
@@ -257,7 +267,7 @@ function colorInCountriesWithValues(
const svg = d3.select(element);
return svg
- .selectAll("path")
+ .selectAll('path[data-country-path="true"]')
.style("fill", (countryPath) => {
const country = getCountryByCountryPath(countryPath);
if (!country?.count) {
From eac747849b6085be809b5fd1d48711f75ae01c47 Mon Sep 17 00:00:00 2001
From: Owen
Date: Sat, 11 Apr 2026 14:12:18 -0700
Subject: [PATCH 6/8] Restrict namespaces to paid plans due to abuse
---
messages/en-US.json | 3 ++-
server/lib/billing/tierMatrix.ts | 6 +++--
.../checkDomainNamespaceAvailability.ts | 23 +++++++++++++++-
.../routers/domain/listDomainNamespaces.ts | 26 ++++++++++++++++++-
server/routers/resource/createResource.ts | 25 +++++++++++++++++-
server/routers/resource/updateResource.ts | 26 +++++++++++++++++--
src/components/DomainPicker.tsx | 22 ++++++++++++++++
7 files changed, 123 insertions(+), 8 deletions(-)
diff --git a/messages/en-US.json b/messages/en-US.json
index 9fa4e730d..5c86aabec 100644
--- a/messages/en-US.json
+++ b/messages/en-US.json
@@ -2113,7 +2113,8 @@
"addDomainToEnableCustomAuthPages": "Users will be able to access the organization's login page and complete resource authentication using this domain.",
"selectDomainForOrgAuthPage": "Select a domain for the organization's authentication page",
"domainPickerProvidedDomain": "Provided Domain",
- "domainPickerFreeProvidedDomain": "Free Provided Domain",
+ "domainPickerFreeProvidedDomain": "Provided Domain",
+ "domainPickerFreeDomainsPaidFeature": "Provided domains are a paid feature. Subscribe to get a domain included with your plan — no need to bring your own.",
"domainPickerVerified": "Verified",
"domainPickerUnverified": "Unverified",
"domainPickerManual": "Manual",
diff --git a/server/lib/billing/tierMatrix.ts b/server/lib/billing/tierMatrix.ts
index c76dcd95b..0756ea665 100644
--- a/server/lib/billing/tierMatrix.ts
+++ b/server/lib/billing/tierMatrix.ts
@@ -19,7 +19,8 @@ export enum TierFeature {
SshPam = "sshPam",
FullRbac = "fullRbac",
SiteProvisioningKeys = "siteProvisioningKeys", // handle downgrade by revoking keys if needed
- SIEM = "siem" // handle downgrade by disabling SIEM integrations
+ SIEM = "siem", // handle downgrade by disabling SIEM integrations
+ DomainNamespaces = "domainNamespaces" // handle downgrade by removing custom domain namespaces
}
export const tierMatrix: Record = {
@@ -56,5 +57,6 @@ export const tierMatrix: Record = {
[TierFeature.SshPam]: ["tier1", "tier3", "enterprise"],
[TierFeature.FullRbac]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.SiteProvisioningKeys]: ["tier3", "enterprise"],
- [TierFeature.SIEM]: ["enterprise"]
+ [TierFeature.SIEM]: ["enterprise"],
+ [TierFeature.DomainNamespaces]: ["tier1", "tier2", "tier3", "enterprise"]
};
diff --git a/server/private/routers/domain/checkDomainNamespaceAvailability.ts b/server/private/routers/domain/checkDomainNamespaceAvailability.ts
index db9a4b46a..0bb7f8704 100644
--- a/server/private/routers/domain/checkDomainNamespaceAvailability.ts
+++ b/server/private/routers/domain/checkDomainNamespaceAvailability.ts
@@ -22,11 +22,15 @@ import { OpenAPITags, registry } from "@server/openApi";
import { db, domainNamespaces, resources } from "@server/db";
import { inArray } from "drizzle-orm";
import { CheckDomainAvailabilityResponse } from "@server/routers/domain/types";
+import { build } from "@server/build";
+import { isSubscribed } from "#private/lib/isSubscribed";
+import { tierMatrix } from "@server/lib/billing/tierMatrix";
const paramsSchema = z.strictObject({});
const querySchema = z.strictObject({
- subdomain: z.string()
+ subdomain: z.string(),
+ // orgId: build === "saas" ? z.string() : z.string().optional() // Required for saas, optional otherwise
});
registry.registerPath({
@@ -58,6 +62,23 @@ export async function checkDomainNamespaceAvailability(
}
const { subdomain } = parsedQuery.data;
+ // if (
+ // build == "saas" &&
+ // !isSubscribed(orgId!, tierMatrix.domainNamespaces)
+ // ) {
+ // // return not available
+ // return response(res, {
+ // data: {
+ // available: false,
+ // options: []
+ // },
+ // success: true,
+ // error: false,
+ // message: "Your current subscription does not support custom domain namespaces. Please upgrade to access this feature.",
+ // status: HttpCode.OK
+ // });
+ // }
+
const namespaces = await db.select().from(domainNamespaces);
let possibleDomains = namespaces.map((ns) => {
const desired = `${subdomain}.${ns.domainNamespaceId}`;
diff --git a/server/private/routers/domain/listDomainNamespaces.ts b/server/private/routers/domain/listDomainNamespaces.ts
index 180613a85..5bbd25b1a 100644
--- a/server/private/routers/domain/listDomainNamespaces.ts
+++ b/server/private/routers/domain/listDomainNamespaces.ts
@@ -22,6 +22,9 @@ import { eq, sql } from "drizzle-orm";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
+import { isSubscribed } from "#private/lib/isSubscribed";
+import { build } from "@server/build";
+import { tierMatrix } from "@server/lib/billing/tierMatrix";
const paramsSchema = z.strictObject({});
@@ -37,7 +40,8 @@ const querySchema = z.strictObject({
.optional()
.default("0")
.transform(Number)
- .pipe(z.int().nonnegative())
+ .pipe(z.int().nonnegative()),
+ // orgId: build === "saas" ? z.string() : z.string().optional() // Required for saas, optional otherwise
});
async function query(limit: number, offset: number) {
@@ -99,6 +103,26 @@ export async function listDomainNamespaces(
);
}
+ // if (
+ // build == "saas" &&
+ // !isSubscribed(orgId!, tierMatrix.domainNamespaces)
+ // ) {
+ // return response(res, {
+ // data: {
+ // domainNamespaces: [],
+ // pagination: {
+ // total: 0,
+ // limit,
+ // offset
+ // }
+ // },
+ // success: true,
+ // error: false,
+ // message: "No namespaces found. Your current subscription does not support custom domain namespaces. Please upgrade to access this feature.",
+ // status: HttpCode.OK
+ // });
+ // }
+
const domainNamespacesList = await query(limit, offset);
const [{ count }] = await db
diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts
index 6cff4d23a..e94a5fc10 100644
--- a/server/routers/resource/createResource.ts
+++ b/server/routers/resource/createResource.ts
@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
-import { db, loginPage } from "@server/db";
+import { db, domainNamespaces, loginPage } from "@server/db";
import {
domains,
orgDomains,
@@ -24,6 +24,8 @@ import { build } from "@server/build";
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
import { getUniqueResourceName } from "@server/db/names";
import { validateAndConstructDomain } from "@server/lib/domainUtils";
+import { isSubscribed } from "#dynamic/lib/isSubscribed";
+import { tierMatrix } from "@server/lib/billing/tierMatrix";
const createResourceParamsSchema = z.strictObject({
orgId: z.string()
@@ -193,6 +195,27 @@ async function createHttpResource(
const subdomain = parsedBody.data.subdomain;
const stickySession = parsedBody.data.stickySession;
+ if (
+ build == "saas" &&
+ !isSubscribed(orgId!, tierMatrix.domainNamespaces)
+ ) {
+ // check if this domain id is a namespace domain and if so, reject
+ const domain = await db
+ .select()
+ .from(domainNamespaces)
+ .where(eq(domainNamespaces.domainId, domainId))
+ .limit(1);
+
+ if (domain.length > 0) {
+ return next(
+ createHttpError(
+ HttpCode.BAD_REQUEST,
+ "Your current subscription does not support custom domain namespaces. Please upgrade to access this feature."
+ )
+ );
+ }
+ }
+
// Validate domain and construct full domain
const domainResult = await validateAndConstructDomain(
domainId,
diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts
index 01f3e79ff..8a2df18c3 100644
--- a/server/routers/resource/updateResource.ts
+++ b/server/routers/resource/updateResource.ts
@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
-import { db, loginPage } from "@server/db";
+import { db, domainNamespaces, loginPage } from "@server/db";
import {
domains,
Org,
@@ -25,6 +25,7 @@ import { validateAndConstructDomain } from "@server/lib/domainUtils";
import { build } from "@server/build";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
+import { isSubscribed } from "#dynamic/lib/isSubscribed";
const updateResourceParamsSchema = z.strictObject({
resourceId: z.string().transform(Number).pipe(z.int().positive())
@@ -318,6 +319,27 @@ async function updateHttpResource(
if (updateData.domainId) {
const domainId = updateData.domainId;
+ if (
+ build == "saas" &&
+ !isSubscribed(resource.orgId, tierMatrix.domainNamespaces)
+ ) {
+ // check if this domain id is a namespace domain and if so, reject
+ const domain = await db
+ .select()
+ .from(domainNamespaces)
+ .where(eq(domainNamespaces.domainId, domainId))
+ .limit(1);
+
+ if (domain.length > 0) {
+ return next(
+ createHttpError(
+ HttpCode.BAD_REQUEST,
+ "Your current subscription does not support custom domain namespaces. Please upgrade to access this feature."
+ )
+ );
+ }
+ }
+
// Validate domain and construct full domain
const domainResult = await validateAndConstructDomain(
domainId,
@@ -366,7 +388,7 @@ async function updateHttpResource(
);
}
}
-
+
if (build != "oss") {
const existingLoginPages = await db
.select()
diff --git a/src/components/DomainPicker.tsx b/src/components/DomainPicker.tsx
index afb273b5c..e1ec1062e 100644
--- a/src/components/DomainPicker.tsx
+++ b/src/components/DomainPicker.tsx
@@ -2,6 +2,7 @@
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
+import { Card, CardContent } from "@/components/ui/card";
import {
Command,
CommandEmpty,
@@ -40,9 +41,12 @@ import {
Check,
CheckCircle2,
ChevronsUpDown,
+ KeyRound,
Zap
} from "lucide-react";
import { useTranslations } from "next-intl";
+import { usePaidStatus } from "@/hooks/usePaidStatus";
+import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix";
import { toUnicode } from "punycode";
import { useCallback, useEffect, useMemo, useState } from "react";
@@ -95,6 +99,7 @@ export default function DomainPicker({
const { env } = useEnvContext();
const api = createApiClient({ env });
const t = useTranslations();
+ const { hasSaasSubscription } = usePaidStatus();
const { data = [], isLoading: loadingDomains } = useQuery(
orgQueries.domains({ orgId })
@@ -691,6 +696,23 @@ export default function DomainPicker({
+ {build === "saas" &&
+ !hasSaasSubscription(
+ tierMatrix[TierFeature.DomainNamespaces]
+ ) &&
+ !hideFreeDomain && (
+
+
+
+
+
+ {t("domainPickerFreeDomainsPaidFeature")}
+
+
+
+
+ )}
+
{/*showProvidedDomainSearch && build === "saas" && (
From f4ea572f6b8a06f4ce0f1be91b31a1772357a843 Mon Sep 17 00:00:00 2001
From: Owen
Date: Sat, 11 Apr 2026 16:50:16 -0700
Subject: [PATCH 7/8] Fix #2828
---
src/components/DeviceLoginForm.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/components/DeviceLoginForm.tsx b/src/components/DeviceLoginForm.tsx
index 16e7f2e1f..0a05e46d3 100644
--- a/src/components/DeviceLoginForm.tsx
+++ b/src/components/DeviceLoginForm.tsx
@@ -319,6 +319,7 @@ export default function DeviceLoginForm({
Date: Sat, 11 Apr 2026 16:59:43 -0700
Subject: [PATCH 8/8] Grandfather in old users
---
server/routers/resource/createResource.ts | 41 +++++++++++++----------
server/routers/resource/updateResource.ts | 37 ++++++++++++--------
2 files changed, 46 insertions(+), 32 deletions(-)
diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts
index e94a5fc10..d8820de79 100644
--- a/server/routers/resource/createResource.ts
+++ b/server/routers/resource/createResource.ts
@@ -114,7 +114,10 @@ export async function createResource(
const { orgId } = parsedParams.data;
- if (req.user && (!req.userOrgRoleIds || req.userOrgRoleIds.length === 0)) {
+ if (
+ req.user &&
+ (!req.userOrgRoleIds || req.userOrgRoleIds.length === 0)
+ ) {
return next(
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
);
@@ -195,24 +198,26 @@ async function createHttpResource(
const subdomain = parsedBody.data.subdomain;
const stickySession = parsedBody.data.stickySession;
- if (
- build == "saas" &&
- !isSubscribed(orgId!, tierMatrix.domainNamespaces)
- ) {
- // check if this domain id is a namespace domain and if so, reject
- const domain = await db
- .select()
- .from(domainNamespaces)
- .where(eq(domainNamespaces.domainId, domainId))
- .limit(1);
+ if (build == "saas" && !isSubscribed(orgId!, tierMatrix.domainNamespaces)) {
+ // grandfather in existing users
+ const lastAllowedDate = new Date("2026-04-12");
+ const userCreatedDate = new Date(req.user?.dateCreated || new Date());
+ if (userCreatedDate > lastAllowedDate) {
+ // check if this domain id is a namespace domain and if so, reject
+ const domain = await db
+ .select()
+ .from(domainNamespaces)
+ .where(eq(domainNamespaces.domainId, domainId))
+ .limit(1);
- if (domain.length > 0) {
- return next(
- createHttpError(
- HttpCode.BAD_REQUEST,
- "Your current subscription does not support custom domain namespaces. Please upgrade to access this feature."
- )
- );
+ if (domain.length > 0) {
+ return next(
+ createHttpError(
+ HttpCode.BAD_REQUEST,
+ "Your current subscription does not support custom domain namespaces. Please upgrade to access this feature."
+ )
+ );
+ }
}
}
diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts
index 8a2df18c3..07e566194 100644
--- a/server/routers/resource/updateResource.ts
+++ b/server/routers/resource/updateResource.ts
@@ -121,7 +121,9 @@ const updateHttpResourceBodySchema = z
if (data.headers) {
// HTTP header values must be visible ASCII or horizontal whitespace, no control chars (RFC 7230)
const validHeaderValue = /^[\t\x20-\x7E]*$/;
- return data.headers.every((h) => validHeaderValue.test(h.value));
+ return data.headers.every((h) =>
+ validHeaderValue.test(h.value)
+ );
}
return true;
},
@@ -323,20 +325,27 @@ async function updateHttpResource(
build == "saas" &&
!isSubscribed(resource.orgId, tierMatrix.domainNamespaces)
) {
- // check if this domain id is a namespace domain and if so, reject
- const domain = await db
- .select()
- .from(domainNamespaces)
- .where(eq(domainNamespaces.domainId, domainId))
- .limit(1);
+ // grandfather in existing users
+ const lastAllowedDate = new Date("2026-04-12");
+ const userCreatedDate = new Date(
+ req.user?.dateCreated || new Date()
+ );
+ if (userCreatedDate > lastAllowedDate) {
+ // check if this domain id is a namespace domain and if so, reject
+ const domain = await db
+ .select()
+ .from(domainNamespaces)
+ .where(eq(domainNamespaces.domainId, domainId))
+ .limit(1);
- if (domain.length > 0) {
- return next(
- createHttpError(
- HttpCode.BAD_REQUEST,
- "Your current subscription does not support custom domain namespaces. Please upgrade to access this feature."
- )
- );
+ if (domain.length > 0) {
+ return next(
+ createHttpError(
+ HttpCode.BAD_REQUEST,
+ "Your current subscription does not support custom domain namespaces. Please upgrade to access this feature."
+ )
+ );
+ }
}
}