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:
mrhid6
2026-07-26 23:41:43 +01:00
co-authored by Claude Opus 5
parent 73b6b548f0
commit 3fc726da9e
3 changed files with 176 additions and 40 deletions
+6 -4
View File
@@ -68,7 +68,8 @@ func issue(args []string) {
instanceID := fs.String("instance-id", "", "instance UUID the licence is bound to (required)")
instanceName := fs.String("instance-name", "", "display name")
accountID := fs.String("account-id", "", "admin-side account id, optional")
tier := fs.String("tier", "", "free | professional | self_hosted (required)")
tier := fs.String("tier", "", "free | professional | enterprise (required)")
deployment := fs.String("deployment", "cloud", "cloud | self_hosted")
term := fs.String("term", "1y", "1m or 1y")
expires := fs.String("expires", "", "explicit RFC3339 expiry, overrides --term")
out := fs.String("out", "", "write the blob to this file instead of stdout")
@@ -78,9 +79,9 @@ func issue(args []string) {
fatal("--instance-id and --tier are required")
}
plan, ok := license.PlanFor(*tier)
plan, ok := license.PlanFor(*deployment, *tier)
if !ok {
fatal("unknown tier %q", *tier)
fatal("no plan for deployment %q tier %q", *deployment, *tier)
}
key := os.Getenv("LICENSE_SIGNING_KEY")
@@ -107,7 +108,7 @@ func issue(args []string) {
// Self Hosted is sold annually only, so the window in which a cancelled
// licence keeps working is bounded at a year.
if plan.Tier == license.TierSelfHosted && *term == "1m" && *expires == "" {
if plan.Deployment == license.DeploymentSelfHosted && *term == "1m" && *expires == "" {
fatal("self_hosted is annual only; use --term=1y or an explicit --expires")
}
@@ -123,6 +124,7 @@ func issue(args []string) {
InstanceName: name,
Tier: plan.Tier,
Deployment: plan.Deployment,
SupportLevel: plan.SupportLevel,
IssuedAt: now,
ExpiresAt: exp,
Limits: plan.Limits,