// Package provision holds the tenant creation rules shared by the control // plane and sitesvc. // // These rules used to be duplicated: the control plane owned one copy and // sitesvc mirrored it by hand. The copies had already drifted — sitesvc retried // on a lost slug race while the control plane returned an error. This package // is the single definition; neither service may reimplement any of it. package provision import ( "fmt" "regexp" "strings" ) const ( MinSlugLength = 3 MaxSlugLength = 40 ) var slugStrip = regexp.MustCompile(`[^a-z0-9]+`) // ReservedSlugs are subdomain labels the platform needs for itself. var ReservedSlugs = map[string]bool{ "www": true, "api": true, "app": true, "admin": true, "auth": true, "install": true, "static": true, "_next": true, "default": true, } // Slugify lowercases a name and collapses every run of non-alphanumeric // characters into a single hyphen, trimming hyphens from both ends. func Slugify(name string) string { s := strings.ToLower(name) s = slugStrip.ReplaceAllString(s, "-") return strings.Trim(s, "-") } // BaseSlug turns a name into a validated slug stem, or explains why it cannot. func BaseSlug(name string) (string, error) { base := Slugify(name) if len(base) < MinSlugLength { return "", fmt.Errorf("organisation name too short (slug must be at least %d characters)", MinSlugLength) } if len(base) > MaxSlugLength { base = base[:MaxSlugLength] } if ReservedSlugs[base] { return "", fmt.Errorf("that organisation name is reserved") } return base, nil } // NextSlug returns the candidate slug for a given attempt. Attempt 1 is the // base itself; later attempts append a counter. func NextSlug(base string, attempt int) string { if attempt < 2 { return base } return fmt.Sprintf("%s-%d", base, attempt) }