feat(license): add lkctl for issuing licences by hand

This commit is contained in:
2026-07-24 14:59:13 +01:00
parent 968408b955
commit 4f1fce32c1
+176
View File
@@ -0,0 +1,176 @@
// Command lkctl issues and inspects Vantage licences by hand.
//
// lkctl keypair
// lkctl issue --instance-id=<uuid> --instance-name="Acme" --tier=professional --term=1y
// lkctl inspect <blob-or-file>
//
// issue reads the signing key from LICENSE_SIGNING_KEY.
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"strings"
"time"
"github.com/google/uuid"
"github.com/hyperboloide/lk"
"github.com/mrhid6/vantage/shared/license"
)
func main() {
if len(os.Args) < 2 {
usage()
}
switch os.Args[1] {
case "keypair":
keypair()
case "issue":
issue(os.Args[2:])
case "inspect":
inspect(os.Args[2:])
default:
usage()
}
}
func usage() {
fmt.Fprintln(os.Stderr, "usage: lkctl keypair | issue | inspect")
os.Exit(2)
}
func keypair() {
priv, err := lk.NewPrivateKey()
if err != nil {
fatal("generate key: %v", err)
}
privStr, err := priv.ToB32String()
if err != nil {
fatal("encode private key: %v", err)
}
// PublicKey.ToB32String returns one value, unlike its private counterpart.
pubStr := priv.GetPublicKey().ToB32String()
fmt.Println("PRIVATE KEY (store in a password manager and in the admin service's")
fmt.Println("LICENSE_SIGNING_KEY; back it up in two places, it cannot be recovered):")
fmt.Println()
fmt.Println(privStr)
fmt.Println()
fmt.Println("PUBLIC KEY (paste into trustedPublicKeys in shared/license/keys.go):")
fmt.Println()
fmt.Println(pubStr)
}
func issue(args []string) {
fs := flag.NewFlagSet("issue", flag.ExitOnError)
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)")
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")
fs.Parse(args)
if *instanceID == "" || *tier == "" {
fatal("--instance-id and --tier are required")
}
plan, ok := license.PlanFor(*tier)
if !ok {
fatal("unknown tier %q", *tier)
}
key := os.Getenv("LICENSE_SIGNING_KEY")
if key == "" {
fatal("LICENSE_SIGNING_KEY is not set")
}
now := time.Now().UTC()
var exp time.Time
switch {
case *expires != "":
t, err := time.Parse(time.RFC3339, *expires)
if err != nil {
fatal("parse --expires: %v", err)
}
exp = t.UTC()
case *term == "1m":
exp = now.AddDate(0, 1, 0)
case *term == "1y":
exp = now.AddDate(1, 0, 0)
default:
fatal("--term must be 1m or 1y")
}
// 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 == "" {
fatal("self_hosted is annual only; use --term=1y or an explicit --expires")
}
name := *instanceName
if name == "" {
name = *instanceID
}
l := license.License{
ID: uuid.NewString(),
InstanceID: *instanceID,
AccountID: *accountID,
InstanceName: name,
Tier: plan.Tier,
Deployment: plan.Deployment,
IssuedAt: now,
ExpiresAt: exp,
Limits: plan.Limits,
Features: plan.Features,
}
blob, err := license.Sign(l, key)
if err != nil {
fatal("%v", err)
}
if *out != "" {
if err := os.WriteFile(*out, []byte(blob+"\n"), 0o600); err != nil {
fatal("write %s: %v", *out, err)
}
fmt.Fprintf(os.Stderr, "wrote %s (tier=%s deployment=%s expires=%s)\n",
*out, l.Tier, l.Deployment, l.ExpiresAt.Format(time.RFC3339))
return
}
fmt.Println(blob)
}
func inspect(args []string) {
if len(args) < 1 {
fatal("usage: lkctl inspect <blob-or-file>")
}
blob := args[0]
if b, err := os.ReadFile(blob); err == nil {
blob = strings.TrimSpace(string(b))
}
l, err := license.Parse(blob)
if err != nil {
fatal("%v", err)
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(l); err != nil {
fatal("%v", err)
}
if time.Now().After(l.ExpiresAt) {
fmt.Fprintf(os.Stderr, "\nNOTE: expired %s\n", l.ExpiresAt.Format(time.RFC3339))
}
}
func fatal(format string, args ...any) {
fmt.Fprintf(os.Stderr, format+"\n", args...)
os.Exit(1)
}