Folders and files
| Name | Name | Last commit date | ||
|---|---|---|---|---|
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
}