package licensing import ( "fmt" "os" "strconv" "strings" "time" ) // Env var names — keep these in one place so README and code stay in sync. const ( EnvSignPassword = "YAMA_SIGN_PASSWORD" // LocalSigner master HMAC key EnvLicenseServer = "YAMA_LICENSE_SERVER" // RemoteSigner: License Server base URL EnvLicenseToken = "YAMA_LICENSE_TOKEN" // RemoteSigner: customer JWT EnvLicensePubKeyPath = "YAMA_LICENSE_PUBLIC_KEY" // LocalSigner-as-LS: RSA public key PEM path EnvLicenseHTTPAddr = "YAMA_LICENSE_HTTP_ADDR" // LocalSigner-as-LS: listen address, e.g. ":8443" EnvLicenseOfflineHrs = "YAMA_LICENSE_OFFLINE_HRS" // RemoteSigner: cache TTL hours (default 24) ) // DefaultOfflineGrace mirrors the "24 hours" decision recorded in the // project memory's licensing design. const DefaultOfflineGrace = 24 * time.Hour // Mode is what NewFromEnv inspected to pick a Signer; useful for logging. type Mode int const ( ModeLocal Mode = iota ModeRemote ModeNoOp ) func (m Mode) String() string { switch m { case ModeLocal: return "local" case ModeRemote: return "remote" default: return "noop" } } // SelectedMode reports which mode NewFromEnv picked based on env vars, // without actually constructing a signer. Useful for startup banners. func SelectedMode() Mode { if os.Getenv(EnvSignPassword) != "" { return ModeLocal } if os.Getenv(EnvLicenseServer) != "" && os.Getenv(EnvLicenseToken) != "" { return ModeRemote } return ModeNoOp } // NewFromEnv builds the Signer chosen by env vars: // - YAMA_SIGN_PASSWORD set → LocalSigner // - YAMA_LICENSE_SERVER + YAMA_LICENSE_TOKEN set → RemoteSigner // - neither → NoOpSigner // // If both LocalSigner and RemoteSigner vars are set, LocalSigner wins // (an operator's own server should never accidentally call out to itself). // In that case we log a loud warning so the operator notices the dead config. // // may be nil — RemoteSigner falls back to a silent logger internally. func NewFromEnv(lg Logger) (Signer, Mode, error) { server := os.Getenv(EnvLicenseServer) token := os.Getenv(EnvLicenseToken) pwd := os.Getenv(EnvSignPassword) if pwd != "" { if (server != "" || token != "") && lg != nil { lg.Warn("%s is set; ignoring %s/%s (LocalSigner wins) — "+ "if you meant to be a customer deployment, unset %s", EnvSignPassword, EnvLicenseServer, EnvLicenseToken, EnvSignPassword) } s, err := NewLocal(pwd) if err != nil { return nil, ModeNoOp, err } return s, ModeLocal, nil } if server != "" && token != "" { if err := ValidateRemoteURL(server); err != nil { return nil, ModeNoOp, fmt.Errorf("%s rejected: %w", EnvLicenseServer, err) } grace := DefaultOfflineGrace if hrs := os.Getenv(EnvLicenseOfflineHrs); hrs != "" { n, err := strconv.Atoi(strings.TrimSpace(hrs)) if err != nil { return nil, ModeNoOp, fmt.Errorf( "%s must be an integer (hours), got %q", EnvLicenseOfflineHrs, hrs) } if n < 0 { return nil, ModeNoOp, fmt.Errorf( "%s must be >= 0, got %d", EnvLicenseOfflineHrs, n) } grace = time.Duration(n) * time.Hour } return NewRemote(server, token, grace, lg), ModeRemote, nil } if server != "" || token != "" { // Partial config is almost certainly a misconfiguration — fail loudly // rather than silently degrading to NoOp. return nil, ModeNoOp, fmt.Errorf( "%s and %s must be set together (got %s=%q %s=%q)", EnvLicenseServer, EnvLicenseToken, EnvLicenseServer, server, EnvLicenseToken, token) } return NewNoOp(), ModeNoOp, nil } // LicenseServerFromEnv builds the License Server HTTP handler if (and only // if) the operator wants to expose it: requires both YAMA_LICENSE_PUBLIC_KEY // (to verify customer JWTs) and YAMA_LICENSE_HTTP_ADDR (listen address) set, // AND the Signer must be a *LocalSigner (RemoteSigner customer deployments // must never serve a License Server — they don't have the master key). // // Returns (nil, "", nil) if not configured. Returns (server, addr, nil) when // configured. Returns error on bad / partial config. func LicenseServerFromEnv(signer Signer, lg Logger) (*LicenseServer, string, error) { pubPath := os.Getenv(EnvLicensePubKeyPath) addr := os.Getenv(EnvLicenseHTTPAddr) if pubPath == "" && addr == "" { return nil, "", nil // not configured — fine } if pubPath == "" || addr == "" { return nil, "", fmt.Errorf( "%s and %s must be set together to enable License Server (got %s=%q %s=%q)", EnvLicensePubKeyPath, EnvLicenseHTTPAddr, EnvLicensePubKeyPath, pubPath, EnvLicenseHTTPAddr, addr) } local, ok := signer.(*LocalSigner) if !ok { return nil, "", fmt.Errorf( "License Server requires LocalSigner mode (set %s); current mode is %s", EnvSignPassword, signer.Mode()) } pubKey, err := LoadRSAPublicKey(pubPath) if err != nil { return nil, "", err } // 5-minute eviction window — twice a typical heartbeat interval. Matches // the discussion in quota.go. return NewLicenseServer(local, pubKey, 5*time.Minute, lg), addr, nil }