Skip to content

Repository files navigation

package main

import (
	"crypto/ed25519"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"
)

// =============================================================================
// CONFIGURATION — Your public key from STEP 1 goes here
// =============================================================================

const ProductPublicKey = "a_6MLNHqq-KJHRR_GYKt0WMonjDZ9_J8NRJRgD7D6MI"

// =============================================================================
// License Data Model
// =============================================================================

type LicenseTier string

const (
	TierTrial      LicenseTier = "trial"
	TierPersonal   LicenseTier = "personal"
	TierTeam       LicenseTier = "team"
	TierEnterprise LicenseTier = "enterprise"
)

type LicenseClaims struct {
	ID        string      `json:"id"`
	Tier      LicenseTier `json:"tier"`
	Owner     string      `json:"owner"` // customer email
	IssuedAt  int64       `json:"iat"`   // unix timestamp
	ExpiresAt int64       `json:"exp"`   // 0 = never
	MaxRows   int         `json:"max_rows"`
	Features  []string    `json:"features"`
}

type License struct {
	Claims    LicenseClaims
	Raw       string
	Signature []byte
	Valid     bool
	Error     string
}

// Feature flags
const (
	FeatureTUI       = "tui"
	FeatureExport    = "export"
	FeatureSchema    = "schema"
	FeatureTemplates = "templates"
	FeaturePreview   = "preview"
	FeatureDocker    = "docker"
	FeatureUnlimited = "unlimited"
)

// Tier defaults
var tierFeatures = map[LicenseTier][]string{
	TierTrial:      {FeatureTUI, FeatureExport, FeatureSchema, FeatureTemplates, FeaturePreview, FeatureDocker},
	TierPersonal:   {FeatureTUI, FeatureExport, FeaturePreview},
	TierTeam:       {FeatureTUI, FeatureExport, FeatureSchema, FeatureTemplates, FeaturePreview, FeatureDocker},
	TierEnterprise: {FeatureTUI, FeatureExport, FeatureSchema, FeatureTemplates, FeaturePreview, FeatureDocker, FeatureUnlimited},
}

var tierMaxRows = map[LicenseTier]int{
	TierTrial:      10000,
	TierPersonal:   100000,
	TierTeam:       1000000,
	TierEnterprise: 0, // unlimited
}

// =============================================================================
// Public API
// =============================================================================

func VerifyLocalLicense() bool {
	lic := LoadAndVerifyLicense()
	return lic.Valid
}

func LoadAndVerifyLicense() *License {
	homeDir, err := os.UserHomeDir()
	if err != nil {
		return trialLicense("unable to locate home directory")
	}

	licensePath := filepath.Join(homeDir, ".datamocker_license")
	data, err := os.ReadFile(licensePath)
	if err != nil {
		// No license file → trial mode
		return trialLicense("no license file found")
	}

	lic := verifyLicenseString(string(data))
	if lic.Valid {
		return lic
	}

	// File exists but is cryptographically invalid → hard stop
	return lic
}

// =============================================================================
// Trial Engine (7 days from first run, persisted to disk)
// =============================================================================

func trialLicense(reason string) *License {
	homeDir, _ := os.UserHomeDir()
	trialFile := filepath.Join(homeDir, ".datamocker_trial")

	var start time.Time
	if data, err := os.ReadFile(trialFile); err == nil {
		if ts, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 64); err == nil {
			start = time.Unix(ts, 0)
		}
	}

	if start.IsZero() {
		start = time.Now()
		_ = os.WriteFile(trialFile, []byte(fmt.Sprintf("%d", start.Unix())), 0644)
	}

	expires := start.AddDate(0, 0, 7).Unix()
	now := time.Now().Unix()

	valid := now <= expires
	errorMsg := reason
	if !valid {
		errorMsg = "trial expired — purchase a license at https://datamocker.dev"
	}

	return &License{
		Claims: LicenseClaims{
			ID:        "trial",
			Tier:      TierTrial,
			Owner:     "trial_user",
			IssuedAt:  start.Unix(),
			ExpiresAt: expires,
			MaxRows:   tierMaxRows[TierTrial],
			Features:  tierFeatures[TierTrial],
		},
		Valid: valid,
		Error: errorMsg,
	}
}

// =============================================================================
// Cryptographic Verification
// =============================================================================

func verifyLicenseString(licStr string) *License {
	lic := &License{Raw: licStr}

	parts := strings.SplitN(licStr, ".", 2)
	if len(parts) != 2 {
		lic.Error = "invalid license format"
		return lic
	}

	payloadB64, sigB64 := parts[0], parts[1]

	payload, err := base64.RawURLEncoding.DecodeString(payloadB64)
	if err != nil {
		lic.Error = "invalid license payload encoding"
		return lic
	}

	sig, err := base64.RawURLEncoding.DecodeString(sigB64)
	if err != nil {
		lic.Error = "invalid license signature encoding"
		return lic
	}
	lic.Signature = sig

	var claims LicenseClaims
	if err := json.Unmarshal(payload, &claims); err != nil {
		lic.Error = "invalid license payload"
		return lic
	}
	lic.Claims = claims

	// Time checks
	now := time.Now().Unix()
	if claims.ExpiresAt > 0 && now > claims.ExpiresAt {
		lic.Error = "license expired"
		return lic
	}
	if claims.IssuedAt > now {
		lic.Error = "license not yet valid"
		return lic
	}

	// Dev mode: placeholder key accepts any well-formed license
	if ProductPublicKey == "YOUR_BASE64_PUBLIC_KEY_HERE" {
		lic.Valid = true
		return lic
	}

	// Cryptographic verification
	pubKeyBytes, err := base64.RawURLEncoding.DecodeString(ProductPublicKey)
	if err != nil || len(pubKeyBytes) != ed25519.PublicKeySize {
		lic.Error = "invalid public key configuration"
		return lic
	}

	if !ed25519.Verify(ed25519.PublicKey(pubKeyBytes), payload, sig) {
		lic.Error = "license signature invalid (tampered or forged)"
		return lic
	}

	lic.Valid = true
	return lic
}

// =============================================================================
// Feature & Tier Enforcement
// =============================================================================

func (l *License) Can(feature string) bool {
	if !l.Valid {
		return false
	}
	for _, f := range l.Claims.Features {
		if f == feature || f == FeatureUnlimited {
			return true
		}
	}
	for _, f := range tierFeatures[l.Claims.Tier] {
		if f == feature || f == FeatureUnlimited {
			return true
		}
	}
	return false
}

func (l *License) MaxAllowedRows() int {
	if l.Claims.MaxRows > 0 {
		return l.Claims.MaxRows
	}
	if max, ok := tierMaxRows[l.Claims.Tier]; ok && max > 0 {
		return max
	}
	return 10000
}

func (l *License) IsTrial() bool    { return l.Claims.Tier == TierTrial }
func (l *License) TierName() string { return strings.Title(string(l.Claims.Tier)) }
func (l *License) DaysRemaining() int {
	if l.Claims.ExpiresAt == 0 {
		return -1
	}
	d := int(time.Until(time.Unix(l.Claims.ExpiresAt, 0)).Hours() / 24)
	if d < 0 {
		return 0
	}
	return d
}

func checkLicenseFeature(feature string) error {
	lic := LoadAndVerifyLicense()
	if !lic.Valid {
		return fmt.Errorf("license invalid: %s", lic.Error)
	}
	if !lic.Can(feature) {
		return fmt.Errorf("%q not available in %s tier — upgrade at https://datamocker.dev", feature, lic.TierName())
	}
	return nil
}

func checkLicenseRows(requested int) error {
	lic := LoadAndVerifyLicense()
	if !lic.Valid {
		return fmt.Errorf("license invalid: %s", lic.Error)
	}
	max := lic.MaxAllowedRows()
	if max > 0 && requested > max {
		return fmt.Errorf("%s tier limited to %s rows (requested %s). Upgrade at https://datamocker.dev",
			lic.TierName(), formatNumber(max), formatNumber(requested))
	}
	return nil
}

About

Generate millions of realistic, referentially-correct mock database rows in seconds. Interactive TUI, custom YAML schemas, preset templates.

Topics

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages