License Verification
How to check and verify licenses offline and online
License Verification
Tuish uses a hybrid verification approach: cryptographic offline verification first, with optional online validation.
How It Works
-
Offline Verification - Licenses are cryptographically signed with Ed25519. The SDK verifies the signature locally without any network call.
-
Machine Binding - Licenses are bound to a machine on first validation (server-side binding). See Machine Binding below.
-
Cache Refresh - Valid licenses are cached locally and refreshed online every 24 hours.
-
Fallback - If online validation fails, the SDK trusts the offline verification result.
Basic Usage
const result = await tuish.checkLicense();
if (result.valid) {
console.log('License is valid');
console.log('Verified offline:', result.offlineVerified);
console.log('License details:', result.license);
} else {
console.log('License invalid:', result.reason);
}ctx := context.Background()
result, err := sdk.CheckLicense(ctx)
if err != nil {
log.Fatal(err)
}
if result.Valid {
fmt.Println("License is valid")
fmt.Printf("Verified offline: %v\n", result.OfflineVerified)
fmt.Printf("License ID: %s\n", result.License.ID)
fmt.Printf("Features: %v\n", result.License.Features)
} else {
fmt.Printf("License invalid: %s\n", result.Reason)
}let result = tuish.check_license();
if result.valid {
println!("License is valid");
println!("Verified offline: {}", result.offline_verified);
if let Some(license) = &result.license {
println!("License ID: {}", license.id);
println!("Features: {:?}", license.features);
}
} else {
if let Some(reason) = &result.reason {
println!("License invalid: {:?}", reason);
}
}result = client.check_license()
if result.valid:
print("License is valid")
print(f"Verified offline: {result.offline_verified}")
print(f"License ID: {result.license.id}")
else:
print(f"License invalid: {result.reason}")License Check Result
interface LicenseCheckResult {
/** Whether the license is valid */
valid: boolean;
/** License payload if valid */
license?: LicenseDetails;
/** Reason for invalid license */
reason?: LicenseInvalidReason;
/** Whether the license was verified offline */
offlineVerified: boolean;
/** Whether license is bound to a machine (server-side) */
machineBound?: boolean;
/** Machine fingerprint the license is bound to */
machineFingerprint?: string;
}
interface LicenseDetails {
id: string;
productId: string;
productName?: string;
features: string[];
status: 'active' | 'expired' | 'revoked';
issuedAt: number;
expiresAt: number | null; // null for perpetual licenses
}
type LicenseInvalidReason =
| 'not_found'
| 'expired'
| 'revoked'
| 'invalid_format'
| 'invalid_signature'
| 'machine_mismatch'
| 'network_error';type LicenseCheckResult struct {
Valid bool
License *LicenseDetails
Reason LicenseInvalidReason
OfflineVerified bool
}
type LicenseDetails struct {
ID string
ProductID string
ProductName string
Features []string
Status LicenseStatus // "active", "expired", "revoked"
IssuedAt int64 // Unix timestamp ms
ExpiresAt *int64 // nil for perpetual licenses
}
const (
ReasonNotFound = "not_found"
ReasonExpired = "expired"
ReasonRevoked = "revoked"
ReasonInvalidFormat = "invalid_format"
ReasonInvalidSignature = "invalid_signature"
ReasonMachineMismatch = "machine_mismatch"
ReasonNetworkError = "network_error"
)pub struct LicenseCheckResult {
pub valid: bool,
pub license: Option<LicenseDetails>,
pub reason: Option<LicenseInvalidReason>,
pub offline_verified: bool,
}
pub struct LicenseDetails {
pub id: String,
pub product_id: String,
pub product_name: Option<String>,
pub features: Vec<String>,
pub status: LicenseStatus,
pub issued_at: i64,
pub expires_at: Option<i64>,
}
pub enum LicenseInvalidReason {
NotFound,
Expired,
Revoked,
InvalidFormat,
InvalidSignature,
WrongMachine,
NetworkError,
}Manual License Management
// Store a license key manually
tuish.storeLicense('eyJhbGciOiJFZER...');
// Get cached license key
const key = tuish.getCachedLicenseKey();
// Clear cached license
tuish.clearLicense();
// Extract license info without verification (display only)
const info = tuish.extractLicenseInfo(licenseKey);// Store a license key manually
err := sdk.StoreLicense("eyJhbGciOiJFZER...")
// Get cached license key
key := sdk.GetCachedLicenseKey()
// Clear cached license
err = sdk.ClearLicense()
// Extract license info without verification (display only)
info, err := sdk.ExtractLicenseInfo(licenseKey)// Save a license key manually
let result = tuish.save_license("eyJhbGciOiJlZDI1NTE5...").await?;
// Get cached license data
let cached = tuish.get_cached_license().await?;
// Clear cached license
tuish.clear_license().await?;
// Check if needs refresh (cache older than 24h)
if let Some(cached) = &cached {
if tuish.needs_refresh(cached) {
println!("License cache is stale");
}
}Machine Binding
Tuish uses server-side machine binding to prevent license sharing across devices.
How It Works
- License Creation - Licenses are created without a machine binding.
- First Validation - On first
POST /v1/licenses/validate, the license is bound to the submitted machine fingerprint. - Subsequent Validations - Future validations check the stored fingerprint. A mismatch returns
reason: 'machine_mismatch'. - Unbinding - Developers can unbind a license via
POST /v1/licenses/:id/unbind(requires API key).
Checking Machine Binding
const result = await tuish.checkLicense();
if (result.valid) {
console.log('Bound to machine:', result.machineBound);
console.log('Machine fingerprint:', result.machineFingerprint);
}
if (result.reason === 'machine_mismatch') {
console.log('License is registered to a different device');
}Unbinding a License (Developer API)
If a user needs to transfer their license to a new machine:
curl -X POST "https://api.tuish.dev/v1/licenses/lic_xxx/unbind" \
-H "Authorization: Bearer tuish_sk_xxx"After unbinding, the next validation will bind the license to the new machine.
Machine Fingerprint
Get the current machine's fingerprint:
const fingerprint = tuish.getMachineFingerprint();
console.log('Machine ID:', fingerprint);fingerprint := sdk.GetMachineFingerprint()
fmt.Printf("Machine ID: %s\n", fingerprint)use tuish::get_machine_fingerprint;
let fingerprint = get_machine_fingerprint();
println!("Machine ID: {}", fingerprint);The fingerprint is a SHA256 hash of: hostname, username, operating system (darwin, linux, windows), and architecture (amd64, arm64, etc.).
Error Handling
const result = await tuish.checkLicense();
if (!result.valid) {
switch (result.reason) {
case 'not_found':
// No license - trigger purchase flow
break;
case 'expired':
// License expired - prompt renewal
break;
case 'revoked':
// License revoked - contact support
break;
case 'machine_mismatch':
// Wrong machine - check device limit
break;
case 'invalid_signature':
// Tampered license - reject
break;
case 'network_error':
// Offline but cache valid - may still work
break;
}
}result, err := sdk.CheckLicense(ctx)
if err != nil {
log.Printf("SDK error: %v", err)
return
}
switch result.Reason {
case tuish.ReasonNotFound:
// No license - trigger purchase flow
case tuish.ReasonExpired:
// License expired - prompt renewal
case tuish.ReasonRevoked:
// License revoked - contact support
case tuish.ReasonMachineMismatch:
// Wrong machine - check device limit
case tuish.ReasonInvalidSignature:
// Tampered license - reject
case tuish.ReasonNetworkError:
// Offline but cache valid - may still work
}use tuish::LicenseInvalidReason;
let result = tuish.check_license();
if !result.valid {
match result.reason {
Some(LicenseInvalidReason::NotFound) => {
println!("No license found - prompt user to purchase");
}
Some(LicenseInvalidReason::Expired) => {
println!("License expired - prompt for renewal");
}
Some(LicenseInvalidReason::WrongMachine) => {
println!("License registered to different machine");
}
Some(LicenseInvalidReason::Revoked) => {
println!("License has been revoked");
}
_ => {
println!("License validation failed");
}
}
}Verification Flow
CheckLicense()
│
▼
┌─────────────────┐
│ Load from disk │
└────────┬────────┘
│
┌────▼────┐
│ Found? │──No──► Return {Valid: false, Reason: "not_found"}
└────┬────┘
│Yes
▼
┌─────────────────┐
│ Verify offline │ (Ed25519 signature check)
└────────┬────────┘
│
┌────▼────┐
│ Valid? │──No──► Handle error (expired, signature, machine)
└────┬────┘
│Yes
▼
┌─────────────────┐
│ Cache fresh? │──Yes──► Return valid result
└────────┬────────┘
│No (>24h)
▼
┌─────────────────┐
│ Validate online │ (API call)
└────────┬────────┘
│
┌────▼────┐
│ Valid? │──Yes──► Update cache, return result
└────┬────┘
│No
▼
Handle revocation or network errorRelated Topics
- Browser Checkout - Purchase flow for new customers
- Terminal Purchase - OTP-based purchase flow