mirror of
https://github.com/luxfi/license.git
synced 2026-10-30 16:21:14 +00:00
- 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.
141 lines
4.2 KiB
Go
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)
|
|
}
|
|
}
|