Pass one at getting it into the db

This commit is contained in:
Owen
2026-04-23 14:05:08 -07:00
parent f03d0cd47f
commit fa117198a0
12 changed files with 377 additions and 44 deletions

View File

@@ -23,7 +23,8 @@ export enum TierFeature {
HTTPPrivateResources = "httpPrivateResources", // handle downgrade by disabling HTTP private resources
DomainNamespaces = "domainNamespaces", // handle downgrade by removing custom domain namespaces
StandaloneHealthChecks = "standaloneHealthChecks",
AlertingRules = "alertingRules"
AlertingRules = "alertingRules",
WildcardSubdomain = "wildcardSubdomain"
}
export const tierMatrix: Record<TierFeature, Tier[]> = {
@@ -64,5 +65,6 @@ export const tierMatrix: Record<TierFeature, Tier[]> = {
[TierFeature.HTTPPrivateResources]: ["tier3", "enterprise"],
[TierFeature.DomainNamespaces]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.StandaloneHealthChecks]: ["tier2", "tier3", "enterprise"],
[TierFeature.AlertingRules]: ["tier2", "tier3", "enterprise"]
[TierFeature.AlertingRules]: ["tier2", "tier3", "enterprise"],
[TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"]
};

View File

@@ -1,5 +1,6 @@
import {
domains,
domainNamespaces,
orgDomains,
Resource,
resourceHeaderAuth,
@@ -236,6 +237,7 @@ export async function updateProxyResources(
fullDomain: http ? resourceData["full-domain"] : null,
subdomain: domain ? domain.subdomain : null,
domainId: domain ? domain.domainId : null,
wildcard: domain ? domain.wildcard : false,
enabled: resourceEnabled,
sso: resourceData.auth?.["sso-enabled"] || false,
skipToIdpId:
@@ -683,6 +685,7 @@ export async function updateProxyResources(
fullDomain: http ? resourceData["full-domain"] : null,
subdomain: domain ? domain.subdomain : null,
domainId: domain ? domain.domainId : null,
wildcard: domain ? domain.wildcard : false,
enabled: resourceEnabled,
sso: resourceData.auth?.["sso-enabled"] || false,
skipToIdpId: resourceData.auth?.["auto-login-idp"] || null,
@@ -1152,7 +1155,9 @@ async function getDomainId(
orgId: string,
fullDomain: string,
trx: Transaction
): Promise<{ subdomain: string | null; domainId: string } | null> {
): Promise<{ subdomain: string | null; domainId: string; wildcard: boolean } | null> {
const isWildcardFullDomain = fullDomain.startsWith("*.");
const possibleDomains = await trx
.select()
.from(domains)
@@ -1165,6 +1170,11 @@ async function getDomainId(
}
const validDomains = possibleDomains.filter((domain) => {
// Wildcard full-domains are not allowed on CNAME domains
if (isWildcardFullDomain && domain.domains.type === "cname") {
return false;
}
if (domain.domains.type == "ns" || domain.domains.type == "wildcard") {
return (
fullDomain === domain.domains.baseDomain ||
@@ -1182,6 +1192,21 @@ async function getDomainId(
const domainSelection = validDomains[0].domains;
const baseDomain = domainSelection.baseDomain;
// Wildcard full-domains are not allowed on namespace (provided/free) domains
if (isWildcardFullDomain) {
const [namespaceDomain] = await trx
.select()
.from(domainNamespaces)
.where(eq(domainNamespaces.domainId, domainSelection.domainId))
.limit(1);
if (namespaceDomain) {
throw new Error(
`Wildcard full-domains are not supported for provided or free domains: ${fullDomain}`
);
}
}
// remove the base domain of the domain
let subdomain = null;
if (fullDomain != baseDomain) {
@@ -1191,6 +1216,7 @@ async function getDomainId(
// Return the first valid domain
return {
subdomain: subdomain,
domainId: domainSelection.domainId
domainId: domainSelection.domainId,
wildcard: isWildcardFullDomain
};
}

View File

@@ -2,6 +2,7 @@ import { z } from "zod";
import { portRangeStringSchema } from "@server/lib/ip";
import { MaintenanceSchema } from "#dynamic/lib/blueprints/MaintenanceSchema";
import { isValidRegionId } from "@server/db/regions";
import { wildcardSubdomainSchema } from "@server/lib/schemas";
export const SiteSchema = z.object({
name: z.string().min(1).max(100),
@@ -319,6 +320,34 @@ export const ResourceSchema = z
message:
"Rules have conflicting or invalid priorities (must be unique, including auto-assigned ones)"
}
)
.refine(
(resource) => {
const fullDomain = resource["full-domain"];
if (!fullDomain || !fullDomain.includes("*")) return true;
// A wildcard full-domain must be of the form *.labels.basedomain
// Extract the leftmost label(s) before the first non-wildcard segment.
// e.g. "*.level1.example.com" → subdomain candidate is "*.level1"
// We do this by finding the base domain: everything after the first
// real (non-wildcard) dot-separated segment pair.
//
// Simple rule: split on ".", first token must be "*", rest must be
// valid hostname labels, and there must be at least 2 remaining labels
// (so the full domain has a real base domain).
const parts = fullDomain.split(".");
if (parts[0] !== "*") return false; // * must be the very first label
if (parts.includes("*", 1)) return false; // no further wildcards
if (parts.length < 3) return false; // need at least *.label.tld
const labelRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$|^[a-zA-Z0-9]$/;
return parts.slice(1).every((label) => labelRegex.test(label));
},
{
path: ["full-domain"],
message:
'Wildcard full-domain must have "*" as the leftmost label only, followed by at least two valid hostname labels (e.g. "*.example.com" or "*.level1.example.com"). Patterns like "*example.com" or "level2.*.example.com" are not supported.'
}
);
export function isTargetsOnlyResource(resource: any): boolean {

View File

@@ -1,7 +1,7 @@
import { db } from "@server/db";
import { domains, orgDomains } from "@server/db";
import { domains, orgDomains, domainNamespaces } from "@server/db";
import { eq, and } from "drizzle-orm";
import { subdomainSchema } from "@server/lib/schemas";
import { subdomainSchema, wildcardSubdomainSchema } from "@server/lib/schemas";
import { fromError } from "zod-validation-error";
export type DomainValidationResult =
@@ -9,6 +9,7 @@ export type DomainValidationResult =
success: true;
fullDomain: string;
subdomain: string | null;
wildcard: boolean;
}
| {
success: false;
@@ -66,6 +67,47 @@ export async function validateAndConstructDomain(
};
}
// Detect wildcard subdomain request
const isWildcard =
subdomain !== undefined &&
subdomain !== null &&
subdomain.includes("*");
// Wildcard subdomains are not allowed on CNAME domains
if (isWildcard && domainRes.domains.type === "cname") {
return {
success: false,
error: "Wildcard subdomains are not supported for CNAME domains. CNAME domains must use a specific hostname."
};
}
// Wildcard subdomains are not allowed on namespace (provided/free) domains
if (isWildcard) {
const [namespaceDomain] = await db
.select()
.from(domainNamespaces)
.where(eq(domainNamespaces.domainId, domainId))
.limit(1);
if (namespaceDomain) {
return {
success: false,
error: "Wildcard subdomains are not supported for provided or free domains. Use a specific subdomain instead."
};
}
}
// Validate wildcard subdomain format
if (isWildcard) {
const parsedWildcard = wildcardSubdomainSchema.safeParse(subdomain);
if (!parsedWildcard.success) {
return {
success: false,
error: fromError(parsedWildcard.error).toString()
};
}
}
// Construct full domain based on domain type
let fullDomain = "";
let finalSubdomain = subdomain;
@@ -81,13 +123,15 @@ export async function validateAndConstructDomain(
finalSubdomain = null; // CNAME domains don't use subdomains
} else if (domainRes.domains.type === "wildcard") {
if (subdomain !== undefined && subdomain !== null) {
// Validate subdomain format for wildcard domains
const parsedSubdomain = subdomainSchema.safeParse(subdomain);
if (!parsedSubdomain.success) {
return {
success: false,
error: fromError(parsedSubdomain.error).toString()
};
if (!isWildcard) {
// Validate regular subdomain format for wildcard domains
const parsedSubdomain = subdomainSchema.safeParse(subdomain);
if (!parsedSubdomain.success) {
return {
success: false,
error: fromError(parsedSubdomain.error).toString()
};
}
}
fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`;
} else {
@@ -100,13 +144,14 @@ export async function validateAndConstructDomain(
finalSubdomain = null;
}
// Convert to lowercase
// Convert to lowercase (preserve * as-is)
fullDomain = fullDomain.toLowerCase();
return {
success: true,
fullDomain,
subdomain: finalSubdomain ?? null
subdomain: isWildcard ? "*" : (finalSubdomain ?? null),
wildcard: isWildcard
};
} catch (error) {
return {

View File

@@ -1,5 +1,41 @@
import { z } from "zod";
/**
* Validates a wildcard subdomain passed as the leftmost component of a full domain.
*
* The value represents everything to the left of the base domain, so when combined
* with e.g. "example.com" it must produce a valid SSL-style wildcard hostname.
*
* Valid:
* "*" → *.example.com
* "*.level1" → *.level1.example.com
*
* Invalid:
* "*example" → *example.com (no dot after *)
* "level2.*.level1" → wildcard not in leftmost position
* "*.level1.*" → multiple wildcards
*/
export const wildcardSubdomainSchema = z
.string()
.refine(
(val) => {
// Must start with "*."; the remainder (if any) must be valid hostname labels.
// A bare "*" is also valid (becomes *.baseDomain directly).
if (val === "*") return true;
if (!val.startsWith("*.")) return false;
const rest = val.slice(2); // everything after "*."
// rest must not be empty, must not contain another "*",
// and every label must be a valid hostname label.
if (!rest || rest.includes("*")) return false;
const labelRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/;
return rest.split(".").every((label) => labelRegex.test(label));
},
{
message:
'Invalid wildcard subdomain. The wildcard "*" must be the leftmost label followed by a dot and valid hostname labels (e.g. "*" or "*.level1"). Patterns like "*example", "level2.*.level1", or multiple wildcards are not supported.'
}
);
export const subdomainSchema = z
.string()
.regex(