Files
zeekay f77da86cda Initial luxfi/license — Ed25519-signed offline license tokens for commercial Lux features.
- Stdlib-only deterministic binary token format (no JWT / CBOR dep).
- Go API: VerifyToken, LoadFromEnv, HasFeature, HasFeatureOrDie.
- Stable C ABI + C++17 RAII wrapper for non-Go commercial binaries (luxcpp/cevm).
- cmd/lux-license: keygen / issue / inspect / verify.
- Embedded development key; warns once at runtime until production key swap.
- 22 Go tests + 1 cgo round-trip test, all passing.
2026-05-15 17:08:32 -07:00

141 lines
4.2 KiB
Go

// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package license
import (
"errors"
"fmt"
"os"
"path/filepath"
"sync"
)
// Env var / file conventions consulted by LoadFromEnv.
const (
EnvVarToken = "LUX_LICENSE" // base64-encoded token, takes priority
EnvVarFile = "LUX_LICENSE_FILE" // path to a token file (binary or base64)
DefaultPath = ".lux/license.jwt" // joined with $HOME if env vars unset
DefaultPathX = ".lux/license" // alternate default; checked if .jwt absent
)
// ErrNoLicense indicates that no license token could be found in any of the
// standard locations.
var ErrNoLicense = errors.New("license: no token found in $LUX_LICENSE, $LUX_LICENSE_FILE, or default path")
// LoadFromEnv discovers and verifies the active license token, returning its
// Claims. Lookup order:
//
// 1. $LUX_LICENSE (base64-encoded)
// 2. $LUX_LICENSE_FILE (binary or base64 file)
// 3. $HOME/.lux/license.jwt
// 4. $HOME/.lux/license
//
// The result is cached for the lifetime of the process; subsequent calls
// return the same Claims without re-reading the disk. Use ResetCache in tests
// to force a fresh load.
func LoadFromEnv() (*Claims, error) {
cache.once.Do(func() {
cache.claims, cache.err = loadOnce()
})
return cache.claims, cache.err
}
// ResetCache clears the in-process cache used by LoadFromEnv / HasFeature.
// Intended for tests; production code should treat the license as immutable
// for the lifetime of the binary.
func ResetCache() {
resetMu.Lock()
defer resetMu.Unlock()
cache = &cacheState{}
}
type cacheState struct {
once sync.Once
claims *Claims
err error
}
var (
resetMu sync.Mutex
cache = &cacheState{}
)
func loadOnce() (*Claims, error) {
if raw := os.Getenv(EnvVarToken); raw != "" {
tok, err := DecodeBase64(raw)
if err != nil {
return nil, fmt.Errorf("%s: %w", EnvVarToken, err)
}
return VerifyToken(tok)
}
if path := os.Getenv(EnvVarFile); path != "" {
return loadFile(path)
}
home, err := os.UserHomeDir()
if err == nil {
for _, sub := range []string{DefaultPath, DefaultPathX} {
p := filepath.Join(home, sub)
if _, err := os.Stat(p); err == nil {
return loadFile(p)
}
}
}
return nil, ErrNoLicense
}
func loadFile(path string) (*Claims, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("license: read %s: %w", path, err)
}
// Try binary first; fall back to base64. Files written by `lux-license issue`
// are base64; binary files exist only when a customer pipes raw bytes.
if c, vErr := VerifyToken(data); vErr == nil {
return c, nil
}
tok, b64Err := DecodeBase64(string(data))
if b64Err != nil {
return nil, fmt.Errorf("license: %s: not a valid binary token, and base64 decode failed: %w", path, b64Err)
}
c, vErr := VerifyToken(tok)
if vErr != nil {
return nil, fmt.Errorf("license: %s: %w", path, vErr)
}
return c, nil
}
// HasFeature returns true if a valid license is loaded and entitles the
// caller to the named scope. Errors during load (missing or invalid license)
// are treated as "feature not enabled" — call LoadFromEnv directly when you
// need the underlying error.
func HasFeature(scope string) bool {
c, err := LoadFromEnv()
if err != nil {
return false
}
return c.HasScope(scope)
}
// HasFeatureOrDie is the strict counterpart to HasFeature. If no license is
// loaded, or the license does not entitle scope, the process is terminated
// with a clear error message via os.Exit(1). Intended for use at startup of
// commercial-only binaries.
func HasFeatureOrDie(scope string) {
c, err := LoadFromEnv()
if err != nil {
fmt.Fprintf(os.Stderr, "FATAL: Lux commercial feature %q requested but no valid license is present.\n", scope)
fmt.Fprintf(os.Stderr, " %v\n", err)
fmt.Fprintf(os.Stderr, " Set $%s (base64) or $%s (path), or install a token at $HOME/%s.\n",
EnvVarToken, EnvVarFile, DefaultPath)
os.Exit(1)
}
if !c.HasScope(scope) {
fmt.Fprintf(os.Stderr,
"FATAL: Lux license for customer %q (id=%s) does not include scope %q.\n"+
" Authorized scopes: %v\n",
c.CustomerID, c.LicenseID, scope, c.Scope)
os.Exit(1)
}
}