Implement IronLicensing Go SDK
- Add core licensing client with validation, activation, deactivation - Add feature checking with hasFeature/requireFeature pattern - Add trial management and in-app purchase support - Add thread-safe operations with sync.RWMutex - Add machine ID persistence for activation tracking - Support both global client and instance-based usage Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d31c1ca575
commit
83ba425a9c
|
|
@ -0,0 +1,15 @@
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.test
|
||||||
|
*.out
|
||||||
|
vendor/
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
.ironlicensing/
|
||||||
312
README.md
312
README.md
|
|
@ -1,2 +1,310 @@
|
||||||
# ironlicensing-go
|
# IronLicensing Go SDK
|
||||||
IronLicensing SDK for Go - Software licensing and activation
|
|
||||||
|
Official Go SDK for [IronLicensing](https://ironlicensing.com) - Software licensing and activation for your applications.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/IronServices/ironlicensing-go
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Using Global Client
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
licensing "github.com/IronServices/ironlicensing-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Initialize the global client
|
||||||
|
err := licensing.Init("pk_live_your_public_key", "your-product-slug")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate a license
|
||||||
|
result := licensing.Validate("IRON-XXXX-XXXX-XXXX-XXXX")
|
||||||
|
if result.Valid {
|
||||||
|
fmt.Println("License is valid!")
|
||||||
|
fmt.Printf("Status: %s\n", result.License.Status)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Validation failed: %s\n", result.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for features
|
||||||
|
if licensing.HasFeature("premium") {
|
||||||
|
fmt.Println("Premium features enabled!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Client Instance
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
licensing "github.com/IronServices/ironlicensing-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
client, err := licensing.NewClient(licensing.Options{
|
||||||
|
PublicKey: "pk_live_your_public_key",
|
||||||
|
ProductSlug: "your-product-slug",
|
||||||
|
Debug: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activate with context
|
||||||
|
ctx := context.Background()
|
||||||
|
result := client.ActivateContext(ctx, "IRON-XXXX-XXXX-XXXX-XXXX", "My Machine")
|
||||||
|
|
||||||
|
if result.Valid {
|
||||||
|
fmt.Printf("Activated! License type: %s\n", result.License.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
```go
|
||||||
|
options := licensing.Options{
|
||||||
|
PublicKey: "pk_live_xxx", // Required
|
||||||
|
ProductSlug: "your-product", // Required
|
||||||
|
APIBaseURL: "https://api.ironlicensing.com", // Default
|
||||||
|
Debug: false, // Enable debug logging
|
||||||
|
EnableOfflineCache: true, // Cache for offline use
|
||||||
|
CacheValidationMinutes: 60, // Cache duration
|
||||||
|
OfflineGraceDays: 7, // Offline grace period
|
||||||
|
HTTPTimeout: 30 * time.Second, // Request timeout
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Functional Options
|
||||||
|
|
||||||
|
```go
|
||||||
|
err := licensing.Init("pk_live_xxx", "product-slug",
|
||||||
|
licensing.WithDebug(true),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## License Validation
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Simple validation
|
||||||
|
result := client.Validate("IRON-XXXX-XXXX-XXXX-XXXX")
|
||||||
|
|
||||||
|
// With context for timeout/cancellation
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
result := client.ValidateContext(ctx, "IRON-XXXX-XXXX-XXXX-XXXX")
|
||||||
|
|
||||||
|
if result.Valid {
|
||||||
|
license := result.License
|
||||||
|
fmt.Printf("License: %s\n", license.Key)
|
||||||
|
fmt.Printf("Status: %s\n", license.Status)
|
||||||
|
fmt.Printf("Type: %s\n", license.Type)
|
||||||
|
fmt.Printf("Activations: %d/%d\n", license.CurrentActivations, license.MaxActivations)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## License Activation
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Simple activation (uses hostname as machine name)
|
||||||
|
result := client.Activate("IRON-XXXX-XXXX-XXXX-XXXX")
|
||||||
|
|
||||||
|
// With custom machine name
|
||||||
|
result := client.ActivateContext(ctx, "IRON-XXXX-XXXX-XXXX-XXXX", "Production Server")
|
||||||
|
|
||||||
|
if result.Valid {
|
||||||
|
fmt.Println("License activated successfully!")
|
||||||
|
|
||||||
|
// View activations
|
||||||
|
for _, activation := range result.Activations {
|
||||||
|
fmt.Printf("- %s (%s)\n", activation.MachineName, activation.Platform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## License Deactivation
|
||||||
|
|
||||||
|
```go
|
||||||
|
if client.Deactivate() {
|
||||||
|
fmt.Println("License deactivated from this machine")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Feature Checking
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Check if feature is available
|
||||||
|
if client.HasFeature("advanced-analytics") {
|
||||||
|
// Enable advanced analytics
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require feature (returns error if not available)
|
||||||
|
if err := client.RequireFeature("export-pdf"); err != nil {
|
||||||
|
fmt.Printf("Feature not available: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get feature details
|
||||||
|
feature := client.GetFeature("max-users")
|
||||||
|
if feature != nil {
|
||||||
|
fmt.Printf("Feature: %s - %s\n", feature.Name, feature.Description)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Trial Management
|
||||||
|
|
||||||
|
```go
|
||||||
|
result := client.StartTrial("user@example.com")
|
||||||
|
if result.Valid {
|
||||||
|
fmt.Println("Trial started!")
|
||||||
|
fmt.Printf("Trial key: %s\n", result.License.Key)
|
||||||
|
if result.License.ExpiresAt != nil {
|
||||||
|
fmt.Printf("Expires: %s\n", result.License.ExpiresAt.Format(time.RFC3339))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## In-App Purchase
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Get available tiers
|
||||||
|
tiers := client.GetTiers()
|
||||||
|
for _, tier := range tiers {
|
||||||
|
fmt.Printf("%s - $%.2f %s\n", tier.Name, tier.Price, tier.Currency)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start checkout
|
||||||
|
checkout := client.StartPurchase("tier-id", "user@example.com")
|
||||||
|
if checkout.Success {
|
||||||
|
fmt.Printf("Checkout URL: %s\n", checkout.CheckoutURL)
|
||||||
|
// Open URL in browser for user to complete purchase
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## License Status
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Get current license
|
||||||
|
license := client.License()
|
||||||
|
if license != nil {
|
||||||
|
fmt.Printf("Licensed to: %s\n", license.Email)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check status
|
||||||
|
status := client.Status()
|
||||||
|
switch status {
|
||||||
|
case licensing.StatusValid:
|
||||||
|
fmt.Println("License is valid")
|
||||||
|
case licensing.StatusExpired:
|
||||||
|
fmt.Println("License has expired")
|
||||||
|
case licensing.StatusTrial:
|
||||||
|
fmt.Println("Running in trial mode")
|
||||||
|
case licensing.StatusNotActivated:
|
||||||
|
fmt.Println("No license activated")
|
||||||
|
default:
|
||||||
|
fmt.Printf("Status: %s\n", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quick checks
|
||||||
|
if client.IsLicensed() {
|
||||||
|
fmt.Println("Application is licensed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if client.IsTrial() {
|
||||||
|
fmt.Println("Running in trial mode")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## License Types
|
||||||
|
|
||||||
|
| Type | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `TypePerpetual` | One-time purchase, never expires |
|
||||||
|
| `TypeSubscription` | Recurring payment, expires if not renewed |
|
||||||
|
| `TypeTrial` | Time-limited trial license |
|
||||||
|
|
||||||
|
## License Statuses
|
||||||
|
|
||||||
|
| Status | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `StatusValid` | License is valid and active |
|
||||||
|
| `StatusExpired` | License has expired |
|
||||||
|
| `StatusSuspended` | License temporarily suspended |
|
||||||
|
| `StatusRevoked` | License permanently revoked |
|
||||||
|
| `StatusTrial` | Active trial license |
|
||||||
|
| `StatusTrialExpired` | Trial period ended |
|
||||||
|
| `StatusNotActivated` | No license activated |
|
||||||
|
|
||||||
|
## Thread Safety
|
||||||
|
|
||||||
|
The client is thread-safe and can be used concurrently from multiple goroutines:
|
||||||
|
|
||||||
|
```go
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
if client.HasFeature("concurrent-feature") {
|
||||||
|
// Safe to call from multiple goroutines
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Validation errors
|
||||||
|
result := client.Validate(licenseKey)
|
||||||
|
if !result.Valid {
|
||||||
|
switch result.Error {
|
||||||
|
case "license_not_found":
|
||||||
|
fmt.Println("Invalid license key")
|
||||||
|
case "license_expired":
|
||||||
|
fmt.Println("Your license has expired")
|
||||||
|
case "max_activations_reached":
|
||||||
|
fmt.Println("No more activations available")
|
||||||
|
default:
|
||||||
|
fmt.Printf("Error: %s\n", result.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feature requirement errors
|
||||||
|
err := client.RequireFeature("premium")
|
||||||
|
if err != nil {
|
||||||
|
if lre, ok := err.(*licensing.LicenseRequiredError); ok {
|
||||||
|
fmt.Printf("Feature '%s' requires a valid license\n", lre.Feature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Machine ID
|
||||||
|
|
||||||
|
The SDK automatically generates and persists a unique machine ID at `~/.ironlicensing/machine_id`. This ID is used for:
|
||||||
|
- Tracking activations per machine
|
||||||
|
- Preventing license sharing
|
||||||
|
- Offline validation
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - see LICENSE file for details.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,266 @@
|
||||||
|
package ironlicensing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNotInitialized = errors.New("ironlicensing: client not initialized")
|
||||||
|
ErrPublicKeyRequired = errors.New("ironlicensing: public key is required")
|
||||||
|
ErrProductSlugRequired = errors.New("ironlicensing: product slug is required")
|
||||||
|
)
|
||||||
|
|
||||||
|
type LicenseRequiredError struct {
|
||||||
|
Feature string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *LicenseRequiredError) Error() string {
|
||||||
|
return fmt.Sprintf("Feature '%s' requires a valid license", e.Feature)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
options Options
|
||||||
|
transport *Transport
|
||||||
|
currentLicense *License
|
||||||
|
licenseKey string
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
var globalClient *Client
|
||||||
|
var globalMu sync.RWMutex
|
||||||
|
|
||||||
|
func NewClient(options Options) (*Client, error) {
|
||||||
|
if options.PublicKey == "" {
|
||||||
|
return nil, ErrPublicKeyRequired
|
||||||
|
}
|
||||||
|
if options.ProductSlug == "" {
|
||||||
|
return nil, ErrProductSlugRequired
|
||||||
|
}
|
||||||
|
opts := options.WithDefaults()
|
||||||
|
c := &Client{
|
||||||
|
options: opts,
|
||||||
|
transport: NewTransport(opts.APIBaseURL, opts.PublicKey, opts.ProductSlug, opts.HTTPTimeout, opts.Debug),
|
||||||
|
}
|
||||||
|
if opts.Debug {
|
||||||
|
fmt.Println("[IronLicensing] Client initialized")
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Init(publicKey, productSlug string, opts ...func(*Options)) error {
|
||||||
|
options := DefaultOptions()
|
||||||
|
options.PublicKey = publicKey
|
||||||
|
options.ProductSlug = productSlug
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&options)
|
||||||
|
}
|
||||||
|
client, err := NewClient(options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
globalMu.Lock()
|
||||||
|
globalClient = client
|
||||||
|
globalMu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithDebug(debug bool) func(*Options) {
|
||||||
|
return func(o *Options) { o.Debug = debug }
|
||||||
|
}
|
||||||
|
|
||||||
|
func getGlobalClient() (*Client, error) {
|
||||||
|
globalMu.RLock()
|
||||||
|
defer globalMu.RUnlock()
|
||||||
|
if globalClient == nil {
|
||||||
|
return nil, ErrNotInitialized
|
||||||
|
}
|
||||||
|
return globalClient, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Validate(licenseKey string) LicenseResult {
|
||||||
|
return c.ValidateContext(context.Background(), licenseKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ValidateContext(ctx context.Context, licenseKey string) LicenseResult {
|
||||||
|
result := c.transport.Validate(ctx, licenseKey)
|
||||||
|
if result.Valid && result.License != nil {
|
||||||
|
c.mu.Lock()
|
||||||
|
c.currentLicense = result.License
|
||||||
|
c.licenseKey = licenseKey
|
||||||
|
c.mu.Unlock()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Activate(licenseKey string) LicenseResult {
|
||||||
|
return c.ActivateContext(context.Background(), licenseKey, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ActivateContext(ctx context.Context, licenseKey, machineName string) LicenseResult {
|
||||||
|
result := c.transport.Activate(ctx, licenseKey, machineName)
|
||||||
|
if result.Valid && result.License != nil {
|
||||||
|
c.mu.Lock()
|
||||||
|
c.currentLicense = result.License
|
||||||
|
c.licenseKey = licenseKey
|
||||||
|
c.mu.Unlock()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Deactivate() bool {
|
||||||
|
return c.DeactivateContext(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) DeactivateContext(ctx context.Context) bool {
|
||||||
|
c.mu.RLock()
|
||||||
|
key := c.licenseKey
|
||||||
|
c.mu.RUnlock()
|
||||||
|
if key == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if c.transport.Deactivate(ctx, key) {
|
||||||
|
c.mu.Lock()
|
||||||
|
c.currentLicense = nil
|
||||||
|
c.licenseKey = ""
|
||||||
|
c.mu.Unlock()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) StartTrial(email string) LicenseResult {
|
||||||
|
return c.StartTrialContext(context.Background(), email)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) StartTrialContext(ctx context.Context, email string) LicenseResult {
|
||||||
|
result := c.transport.StartTrial(ctx, email)
|
||||||
|
if result.Valid && result.License != nil {
|
||||||
|
c.mu.Lock()
|
||||||
|
c.currentLicense = result.License
|
||||||
|
c.licenseKey = result.License.Key
|
||||||
|
c.mu.Unlock()
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) HasFeature(featureKey string) bool {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
if c.currentLicense == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, f := range c.currentLicense.Features {
|
||||||
|
if f.Key == featureKey && f.Enabled {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) RequireFeature(featureKey string) error {
|
||||||
|
if !c.HasFeature(featureKey) {
|
||||||
|
return &LicenseRequiredError{Feature: featureKey}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetFeature(featureKey string) *Feature {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
if c.currentLicense == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, f := range c.currentLicense.Features {
|
||||||
|
if f.Key == featureKey {
|
||||||
|
return &f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) License() *License {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
return c.currentLicense
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Status() LicenseStatus {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
if c.currentLicense != nil {
|
||||||
|
return c.currentLicense.Status
|
||||||
|
}
|
||||||
|
return StatusNotActivated
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) IsLicensed() bool {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
return c.currentLicense != nil && (c.currentLicense.Status == StatusValid || c.currentLicense.Status == StatusTrial)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) IsTrial() bool {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
return c.currentLicense != nil && (c.currentLicense.Status == StatusTrial || c.currentLicense.Type == TypeTrial)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetTiers() []ProductTier {
|
||||||
|
return c.transport.GetTiers(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) StartPurchase(tierID, email string) CheckoutResult {
|
||||||
|
return c.transport.StartCheckout(context.Background(), tierID, email)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global functions
|
||||||
|
func Validate(licenseKey string) LicenseResult {
|
||||||
|
c, err := getGlobalClient()
|
||||||
|
if err != nil {
|
||||||
|
return LicenseResult{Valid: false, Error: err.Error()}
|
||||||
|
}
|
||||||
|
return c.Validate(licenseKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Activate(licenseKey string) LicenseResult {
|
||||||
|
c, err := getGlobalClient()
|
||||||
|
if err != nil {
|
||||||
|
return LicenseResult{Valid: false, Error: err.Error()}
|
||||||
|
}
|
||||||
|
return c.Activate(licenseKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Deactivate() bool {
|
||||||
|
c, err := getGlobalClient()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return c.Deactivate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartTrial(email string) LicenseResult {
|
||||||
|
c, err := getGlobalClient()
|
||||||
|
if err != nil {
|
||||||
|
return LicenseResult{Valid: false, Error: err.Error()}
|
||||||
|
}
|
||||||
|
return c.StartTrial(email)
|
||||||
|
}
|
||||||
|
|
||||||
|
func HasFeature(featureKey string) bool {
|
||||||
|
c, err := getGlobalClient()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return c.HasFeature(featureKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RequireFeature(featureKey string) error {
|
||||||
|
c, err := getGlobalClient()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.RequireFeature(featureKey)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
package ironlicensing
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Options configures the IronLicensing client.
|
||||||
|
type Options struct {
|
||||||
|
PublicKey string
|
||||||
|
ProductSlug string
|
||||||
|
APIBaseURL string
|
||||||
|
Debug bool
|
||||||
|
EnableOfflineCache bool
|
||||||
|
CacheValidationMinutes int
|
||||||
|
OfflineGraceDays int
|
||||||
|
HTTPTimeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultOptions returns the default configuration options.
|
||||||
|
func DefaultOptions() Options {
|
||||||
|
return Options{
|
||||||
|
APIBaseURL: "https://api.ironlicensing.com",
|
||||||
|
Debug: false,
|
||||||
|
EnableOfflineCache: true,
|
||||||
|
CacheValidationMinutes: 60,
|
||||||
|
OfflineGraceDays: 7,
|
||||||
|
HTTPTimeout: 30 * time.Second,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDefaults returns the options with default values applied.
|
||||||
|
func (o Options) WithDefaults() Options {
|
||||||
|
defaults := DefaultOptions()
|
||||||
|
if o.APIBaseURL == "" {
|
||||||
|
o.APIBaseURL = defaults.APIBaseURL
|
||||||
|
}
|
||||||
|
if o.CacheValidationMinutes == 0 {
|
||||||
|
o.CacheValidationMinutes = defaults.CacheValidationMinutes
|
||||||
|
}
|
||||||
|
if o.OfflineGraceDays == 0 {
|
||||||
|
o.OfflineGraceDays = defaults.OfflineGraceDays
|
||||||
|
}
|
||||||
|
if o.HTTPTimeout == 0 {
|
||||||
|
o.HTTPTimeout = defaults.HTTPTimeout
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
module github.com/IronServices/ironlicensing-go
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require github.com/google/uuid v1.6.0
|
||||||
|
|
@ -0,0 +1,193 @@
|
||||||
|
package ironlicensing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Transport struct {
|
||||||
|
baseURL string
|
||||||
|
publicKey string
|
||||||
|
productSlug string
|
||||||
|
debug bool
|
||||||
|
httpClient *http.Client
|
||||||
|
machineID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTransport(baseURL, publicKey, productSlug string, timeout time.Duration, debug bool) *Transport {
|
||||||
|
t := &Transport{
|
||||||
|
baseURL: baseURL,
|
||||||
|
publicKey: publicKey,
|
||||||
|
productSlug: productSlug,
|
||||||
|
debug: debug,
|
||||||
|
httpClient: &http.Client{Timeout: timeout},
|
||||||
|
}
|
||||||
|
t.machineID = t.getMachineID()
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) log(msg string) {
|
||||||
|
if t.debug {
|
||||||
|
fmt.Printf("[IronLicensing] %s\n", msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) getMachineID() string {
|
||||||
|
homeDir, _ := os.UserHomeDir()
|
||||||
|
idPath := filepath.Join(homeDir, ".ironlicensing", "machine_id")
|
||||||
|
if data, err := os.ReadFile(idPath); err == nil {
|
||||||
|
return string(data)
|
||||||
|
}
|
||||||
|
id := uuid.New().String()
|
||||||
|
os.MkdirAll(filepath.Dir(idPath), 0755)
|
||||||
|
os.WriteFile(idPath, []byte(id), 0644)
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) request(ctx context.Context, method, path string, body any) (*http.Response, error) {
|
||||||
|
var reqBody io.Reader
|
||||||
|
if body != nil {
|
||||||
|
data, _ := json.Marshal(body)
|
||||||
|
reqBody = bytes.NewReader(data)
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(ctx, method, t.baseURL+path, reqBody)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("X-Public-Key", t.publicKey)
|
||||||
|
req.Header.Set("X-Product-Slug", t.productSlug)
|
||||||
|
return t.httpClient.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) Validate(ctx context.Context, licenseKey string) LicenseResult {
|
||||||
|
t.log(fmt.Sprintf("Validating: %s...", licenseKey[:min(10, len(licenseKey))]))
|
||||||
|
resp, err := t.request(ctx, "POST", "/api/v1/validate", map[string]string{
|
||||||
|
"licenseKey": licenseKey,
|
||||||
|
"machineId": t.machineID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return LicenseResult{Valid: false, Error: err.Error()}
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
if resp.StatusCode == 200 {
|
||||||
|
var result LicenseResult
|
||||||
|
json.Unmarshal(body, &result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
var errResp struct{ Error string }
|
||||||
|
json.Unmarshal(body, &errResp)
|
||||||
|
return LicenseResult{Valid: false, Error: errResp.Error}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) Activate(ctx context.Context, licenseKey, machineName string) LicenseResult {
|
||||||
|
t.log(fmt.Sprintf("Activating: %s...", licenseKey[:min(10, len(licenseKey))]))
|
||||||
|
if machineName == "" {
|
||||||
|
machineName, _ = os.Hostname()
|
||||||
|
}
|
||||||
|
resp, err := t.request(ctx, "POST", "/api/v1/activate", map[string]string{
|
||||||
|
"licenseKey": licenseKey,
|
||||||
|
"machineId": t.machineID,
|
||||||
|
"machineName": machineName,
|
||||||
|
"platform": runtime.GOOS,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return LicenseResult{Valid: false, Error: err.Error()}
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
if resp.StatusCode == 200 {
|
||||||
|
var result LicenseResult
|
||||||
|
json.Unmarshal(body, &result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
var errResp struct{ Error string }
|
||||||
|
json.Unmarshal(body, &errResp)
|
||||||
|
return LicenseResult{Valid: false, Error: errResp.Error}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) Deactivate(ctx context.Context, licenseKey string) bool {
|
||||||
|
resp, err := t.request(ctx, "POST", "/api/v1/deactivate", map[string]string{
|
||||||
|
"licenseKey": licenseKey,
|
||||||
|
"machineId": t.machineID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
return resp.StatusCode == 200
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) StartTrial(ctx context.Context, email string) LicenseResult {
|
||||||
|
t.log(fmt.Sprintf("Starting trial for: %s", email))
|
||||||
|
resp, err := t.request(ctx, "POST", "/api/v1/trial", map[string]string{
|
||||||
|
"email": email,
|
||||||
|
"machineId": t.machineID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return LicenseResult{Valid: false, Error: err.Error()}
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
if resp.StatusCode == 200 {
|
||||||
|
var result LicenseResult
|
||||||
|
json.Unmarshal(body, &result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
var errResp struct{ Error string }
|
||||||
|
json.Unmarshal(body, &errResp)
|
||||||
|
return LicenseResult{Valid: false, Error: errResp.Error}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) GetTiers(ctx context.Context) []ProductTier {
|
||||||
|
resp, err := t.request(ctx, "GET", "/api/v1/tiers", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode == 200 {
|
||||||
|
var result struct{ Tiers []ProductTier }
|
||||||
|
json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
return result.Tiers
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transport) StartCheckout(ctx context.Context, tierID, email string) CheckoutResult {
|
||||||
|
resp, err := t.request(ctx, "POST", "/api/v1/checkout", map[string]string{
|
||||||
|
"tierId": tierID,
|
||||||
|
"email": email,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return CheckoutResult{Success: false, Error: err.Error()}
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
if resp.StatusCode == 200 {
|
||||||
|
var result CheckoutResult
|
||||||
|
json.Unmarshal(body, &result)
|
||||||
|
result.Success = true
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
var errResp struct{ Error string }
|
||||||
|
json.Unmarshal(body, &errResp)
|
||||||
|
return CheckoutResult{Success: false, Error: errResp.Error}
|
||||||
|
}
|
||||||
|
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
// Package ironlicensing provides an SDK for software licensing.
|
||||||
|
package ironlicensing
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// LicenseStatus represents the status of a license.
|
||||||
|
type LicenseStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusValid LicenseStatus = "valid"
|
||||||
|
StatusExpired LicenseStatus = "expired"
|
||||||
|
StatusSuspended LicenseStatus = "suspended"
|
||||||
|
StatusRevoked LicenseStatus = "revoked"
|
||||||
|
StatusInvalid LicenseStatus = "invalid"
|
||||||
|
StatusTrial LicenseStatus = "trial"
|
||||||
|
StatusTrialExpired LicenseStatus = "trial_expired"
|
||||||
|
StatusNotActivated LicenseStatus = "not_activated"
|
||||||
|
StatusUnknown LicenseStatus = "unknown"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LicenseType represents the type of license.
|
||||||
|
type LicenseType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypePerpetual LicenseType = "perpetual"
|
||||||
|
TypeSubscription LicenseType = "subscription"
|
||||||
|
TypeTrial LicenseType = "trial"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Feature represents a feature in a license.
|
||||||
|
type Feature struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Metadata map[string]any `json:"metadata,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// License represents license information.
|
||||||
|
type License struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Status LicenseStatus `json:"status"`
|
||||||
|
Type LicenseType `json:"type"`
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Company string `json:"company,omitempty"`
|
||||||
|
Features []Feature `json:"features"`
|
||||||
|
MaxActivations int `json:"maxActivations"`
|
||||||
|
CurrentActivations int `json:"currentActivations"`
|
||||||
|
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
LastValidatedAt *time.Time `json:"lastValidatedAt,omitempty"`
|
||||||
|
Metadata map[string]any `json:"metadata,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activation represents activation information.
|
||||||
|
type Activation struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
MachineID string `json:"machineId"`
|
||||||
|
MachineName string `json:"machineName,omitempty"`
|
||||||
|
Platform string `json:"platform,omitempty"`
|
||||||
|
ActivatedAt time.Time `json:"activatedAt"`
|
||||||
|
LastSeenAt time.Time `json:"lastSeenAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LicenseResult represents the result of license validation.
|
||||||
|
type LicenseResult struct {
|
||||||
|
Valid bool `json:"valid"`
|
||||||
|
License *License `json:"license,omitempty"`
|
||||||
|
Activations []Activation `json:"activations,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
Cached bool `json:"cached,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckoutResult represents the result of checkout.
|
||||||
|
type CheckoutResult struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
CheckoutURL string `json:"checkoutUrl,omitempty"`
|
||||||
|
SessionID string `json:"sessionId,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProductTier represents a product tier for purchase.
|
||||||
|
type ProductTier struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Slug string `json:"slug"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
Currency string `json:"currency"`
|
||||||
|
BillingPeriod string `json:"billingPeriod,omitempty"`
|
||||||
|
Features []Feature `json:"features"`
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue