feat(license): two deployments times three tiers, and two new limits
Plans are keyed on (deployment, tier) rather than tier alone, which is what ends Free being cloud-only by construction - there is a self-hosted Free plan now, so the one-per-account rule has to be enforced per deployment instead of falling out of the plan table. tier self_hosted becomes a legacy value no new licence carries. NormaliseTier maps it to self-hosted Professional, which is what it always granted, so blobs we cannot re-sign keep working. Limits.FillUnset exists because a licence signed before a field existed decodes it as 0, and 0 would read as no monitors and an audit log trimmed to nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,15 @@ import "time"
|
||||
const (
|
||||
TierFree = "free"
|
||||
TierProfessional = "professional"
|
||||
TierSelfHosted = "self_hosted"
|
||||
TierEnterprise = "enterprise"
|
||||
|
||||
// TierSelfHosted is LEGACY and no new licence carries it.
|
||||
//
|
||||
// It was a tier when self-hosting was a tier rather than a deployment. Blobs
|
||||
// already signed with it exist and cannot be rewritten, so it stays a
|
||||
// recognised value that NormaliseTier maps forward. Never put it in a plan
|
||||
// row and never offer it in a UI.
|
||||
TierSelfHosted = "self_hosted"
|
||||
|
||||
DeploymentCloud = "cloud"
|
||||
DeploymentSelfHosted = "self_hosted"
|
||||
@@ -25,14 +33,59 @@ const (
|
||||
FeatureOIDC = "oidc" // per-instance single sign-on
|
||||
)
|
||||
|
||||
// Support levels. Carried for display and enforced by nothing — there is no code
|
||||
// path anywhere that branches on these, and there must not be one. They are here
|
||||
// so an air-gapped install can tell its operator who to call without reaching
|
||||
// Vantage HQ.
|
||||
const (
|
||||
SupportCommunity = "community"
|
||||
SupportEmail24x5 = "email_24_5"
|
||||
SupportEmailCall24x7 = "email_call_24_7"
|
||||
)
|
||||
|
||||
// Unlimited is the sentinel for "no cap" in every Limits field.
|
||||
const Unlimited = -1
|
||||
|
||||
// Limits are the countable caps a licence grants.
|
||||
//
|
||||
// Every field is a plain int with Unlimited as the sentinel. AuditRetentionDays
|
||||
// is the odd one out: it bounds a duration rather than a count, and Unlimited
|
||||
// there means "never trim" rather than "no cap".
|
||||
type Limits struct {
|
||||
MaxServers int `json:"max_servers"`
|
||||
MaxSecretGroups int `json:"max_secret_groups"`
|
||||
MaxChannels int `json:"max_channels"`
|
||||
MaxServers int `json:"max_servers"`
|
||||
MaxMonitors int `json:"max_monitors"`
|
||||
MaxSecretGroups int `json:"max_secret_groups"`
|
||||
MaxChannels int `json:"max_channels"`
|
||||
AuditRetentionDays int `json:"audit_retention_days"`
|
||||
}
|
||||
|
||||
// FillUnset replaces any zero field with the same field from base.
|
||||
//
|
||||
// This exists for one reason: a licence signed before a field existed decodes it
|
||||
// as 0, and 0 would read as the most restrictive possible value — no monitors,
|
||||
// and an audit log trimmed to nothing. A blob we cannot re-sign must not be
|
||||
// allowed to mean that.
|
||||
//
|
||||
// The cost is that 0 stops being expressible as a real allowance. No plan grants
|
||||
// zero of anything, so nothing is lost today; a plan that genuinely means zero
|
||||
// must use a negative-free sentinel of its own rather than reintroducing 0 here.
|
||||
func (l Limits) FillUnset(base Limits) Limits {
|
||||
if l.MaxServers == 0 {
|
||||
l.MaxServers = base.MaxServers
|
||||
}
|
||||
if l.MaxMonitors == 0 {
|
||||
l.MaxMonitors = base.MaxMonitors
|
||||
}
|
||||
if l.MaxSecretGroups == 0 {
|
||||
l.MaxSecretGroups = base.MaxSecretGroups
|
||||
}
|
||||
if l.MaxChannels == 0 {
|
||||
l.MaxChannels = base.MaxChannels
|
||||
}
|
||||
if l.AuditRetentionDays == 0 {
|
||||
l.AuditRetentionDays = base.AuditRetentionDays
|
||||
}
|
||||
return l
|
||||
}
|
||||
|
||||
// License is the signed payload.
|
||||
@@ -47,6 +100,7 @@ type License struct {
|
||||
InstanceName string `json:"instance_name"` // display only
|
||||
Tier string `json:"tier"`
|
||||
Deployment string `json:"deployment"`
|
||||
SupportLevel string `json:"support_level,omitempty"` // display only
|
||||
IssuedAt time.Time `json:"issued_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
Limits Limits `json:"limits"`
|
||||
|
||||
Reference in New Issue
Block a user