revshop-license 1.1.1
REVSHOP License Manager - Python Client for Software License Protection
License: MIT
Requires Python: >=3.8
Keywords: license, protection, drm, revshop, software-license, hardware-binding

# REVSHOP License Manager - Python Client

[![PyPI version](https://badge.fury.io/py/revshop-license.svg)](https://badge.fury.io/py/revshop-license)
[![Python versions](https://img.shields.io/pypi/pyversions/revshop-license.svg)](https://pypi.org/project/revshop-license/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

A secure Python client for REVSHOP license protection system. Protect your Python applications with hardware-bound license keys, real-time monitoring, and automatic termination on violations.

## Features

- 🔐 **Hardware ID Binding** - Binds to CPU, motherboard, or machine ID
- 🔄 **Real-time Monitoring** - Automatic status checks every 30 seconds
- 🚫 **Auto-terminate** - Program exits immediately on license violations
- 📴 **No Grace Period** - Strict enforcement, internet required
- 🔒 **Secure Storage** - Encrypted local configuration
- ✅ **Signature Verification** - HMAC-SHA256 response validation
- 🎯 **Simple API** - Easy integration with flexible callbacks

## Installation

```bash
pip install revshop-license
```

## Quick Start

### Basic Usage

```python
from revshop_license import LicenseManager
from datetime import datetime

# Initialize
lm = LicenseManager(
    base_url="https://your-shop.com",
    product_id="your_product_id",
    product_secret="your_product_secret"
)

# Validate license - this returns all license data
result = lm.validate("REV-XXXX-XXXX-XXXX-XXXX")

if result["valid"]:
    # Get license data from result (NOT from Library directly)
    print("License valid!")
    print(f"Product: {result['license_info']['product']['name']}")
    print(f"Activated: {result['activated_at']}")
    print(f"Expires: {result['expires_at']}")
    
    # Calculate days remaining
    if result['expires_at']:
        expires = datetime.fromisoformat(result['expires_at'].replace('Z', '+00:00'))
        days_left = (expires - datetime.now(expires.tzinfo)).days
        print(f"Days remaining: {days_left}")
else:
    print(f"Error: {result['message']}")
```

### Important Notes

- **First activation requires internet** - Cannot use offline mode for first-time activation
- **Auto-exit on violations** - The program will automatically exit if license is revoked/suspended/expired
- **No grace period** - Strict enforcement requires internet connection
- **Must call validate() first** - All license data comes from the `validate()` result

## Understanding the Flow

```
1. Create LicenseManager
   ↓
2. Call validate("REV-XXXX...") 
   ↓ (Server validates and returns data)
3. Get result with all license info:
   - activated_at: when license was first activated
   - expires_at: when license expires
   - license_info: full product details
   - first_activation: True if this is first use
   ↓
4. Use the data to display in your UI
```

## Advanced Usage

### Custom Callbacks (Optional)

By default, the library will exit the program when a license violation is detected without printing anything. You can customize this behavior:

```python
def on_revoked(data):
    # This is called when license is revoked
    # You have about 1 second to show your message before exit
    print("License revoked! Contact support.")
    # Program will exit automatically after callback returns

# Set callbacks BEFORE validate()
lm.set_callbacks(
    on_revoked=on_revoked,
    on_suspended=on_suspended,
    on_expired=on_expired
)

result = lm.validate("REV-XXXX-XXXX-XXXX-XXXX")
```

### Auto Lookup Mode

If you don't know the `product_id`, you can use the auto-lookup feature:

```python
lm, lookup_result = LicenseManager.from_license_key(
    base_url="https://your-shop.com",
    license_key="REV-XXXX-XXXX-XXXX-XXXX",
    product_secret="your_product_secret"
)

if lookup_result["found"]:
    result = lm.validate()
    print(f"Product: {lookup_result['product_name']}")
```

### Complete Example

```python
import sys, time
from datetime import datetime
from revshop_license import LicenseManager

# Configuration (should be encoded in production)
BASE_URL = "https://your-shop.com"
PRODUCT_ID = "your_product_id"
PRODUCT_SECRET = "your_product_secret"

# Create LicenseManager
lm = LicenseManager(
    base_url=BASE_URL,
    product_id=PRODUCT_ID,
    product_secret=PRODUCT_SECRET
)

# Get license key from user
license_key = input("Enter License Key: ").strip()

if not license_key:
    print("License key required!")
    sys.exit(1)

# Validate - this returns ALL license data
result = lm.validate(license_key)

if not result["valid"]:
    print(f"Validation failed: {result['message']}")
    sys.exit(1)

# ✅ Success! Display license information from RESULT
print("\n" + "="*60)
print("LICENSE ACTIVATED")
print("="*60)

# Get product name from result
info = result.get("license_info", {})
print(f"Product: {info.get('product', {}).get('name', 'N/A')}")
print(f"Status: {info.get('status', 'N/A')}")

# Get dates from result (NOT from Library)
if result.get("activated_at"):
    activated = datetime.fromisoformat(result["activated_at"].replace('Z', '+00:00'))
    print(f"Activated: {activated.strftime('%Y-%m-%d %H:%M')}")

if result.get("expires_at"):
    expires = datetime.fromisoformat(result["expires_at"].replace('Z', '+00:00'))
    print(f"Expires: {expires.strftime('%Y-%m-%d %H:%M')}")
    
    # Calculate days remaining
    days_left = (expires - datetime.now(expires.tzinfo)).days
    print(f"Days remaining: {days_left}")
else:
    print("License type: Lifetime")

print("="*60)
print("\nProgram is now running with license protection.")
print("The program will exit automatically if license is revoked.\n")

# Your main application loop
try:
    while True:
        # Your application logic here
        time.sleep(1)
except KeyboardInterrupt:
    lm.stop_monitoring()
    print("\nProgram terminated.")
```

## Common Mistakes

### ❌ Wrong: Trying to get data before validate()
```python
lm = LicenseManager(...)

# This is WRONG - validate() hasn't been called yet!
dates = lm.get_license_dates()  # Returns None or old data
print(dates)

result = lm.validate(license_key)  # Data comes from here!
```

### ✅ Correct: Get data from validate() result
```python
lm = LicenseManager(...)

result = lm.validate(license_key)  # Get data here

if result["valid"]:
    # Use data from result
    print(f"Activated: {result['activated_at']}")
    print(f"Expires: {result['expires_at']}")
```

## API Reference

### LicenseManager

#### Constructor

```python
LicenseManager(
    base_url: str,        # License server URL
    product_id: str,      # Product ID from REVSHOP
    product_secret: str   # Product Secret from REVSHOP Admin
)
```

#### Methods

| Method | Description |
|--------|-------------|
| `validate(license_key=None, force_online=False)` | Validate license key. **Returns all license data** |
| `validate_force_online(license_key=None)` | Force online validation |
| `check_status_now()` | Check status immediately |
| `get_saved_license()` | Get saved license key |
| `save_license(key)` | Save license key |
| `clear_license()` | Clear saved license |
| `deactivate(reason)` | Deactivate on this device |
| `get_status()` | Get current status |
| `get_license_dates()` | Get dates from **cached** config (call validate() first!) |
| `stop_monitoring()` | Stop monitoring threads |
| `set_callbacks(...)` | Set event callbacks |

#### Validation Result

The `validate()` method returns a dictionary with these keys:

```python
{
    "valid": bool,              # Is license valid
    "message": str,             # Status message
    "offline": bool,            # Using offline cache
    "first_activation": bool,   # Is this the first activation
    "activated_at": str,        # Activation timestamp (ISO format)
    "expires_at": str,          # Expiration timestamp (ISO format)
    "license_info": dict        # Full license details including product name
}
```

#### Callbacks

Set custom callbacks for license events:

- `on_revoked` - Called when license is revoked
- `on_suspended` - Called when license is suspended  
- `on_expired` - Called when license expires
- `on_device_removed` - Called when device is removed
- `on_failed` - Called on validation failure
- `on_status_changed` - Called on status change

**Note:** Callbacks are called BEFORE the program exits. You have about 1 second to show your message.

## Configuration

The client stores configuration in:
- Windows: `%USERPROFILE%\.revshop_license\.config.dat`
- Linux/Mac: `~/.revshop_license/.config.dat`

Configuration is encrypted with hardware-based key.

## Error Codes

| Error Code | Description |
|------------|-------------|
| `NOT_ACTIVATED` | License not activated yet (first use) |
| `LICENSE_NOT_FOUND` | License key doesn't exist |
| `LICENSE_REVOKED` | License has been revoked |
| `LICENSE_SUSPENDED` | License has been suspended |
| `LICENSE_EXPIRED` | License has expired |
| `DEVICE_NOT_AUTHORIZED` | This device was removed |
| `DEVICE_LIMIT_EXCEEDED` | Maximum devices reached |
| `INVALID_PRODUCT_SECRET` | Wrong product secret |
| `NETWORK_ERROR` | Cannot connect to server |

## Requirements

- Python 3.8+
- `requests` library (installed automatically)

## Security Notes

- **Encode your credentials** - Use base85 or similar encoding to hide `product_id` and `product_secret` in your code
- **Hardware binding** - License is tied to specific hardware (CPU/Mainboard)
- **No grace period** - Strict enforcement requires internet connection
- **Auto-terminate** - Program exits immediately on violations

## Example: Encoding Configuration

```python
import base64

# Encode your credentials
product_id = "your_product_id"
product_secret = "your_product_secret"
base_url = "https://your-shop.com"

encoded_id = base64.b85encode(product_id.encode()).decode()
encoded_secret = base64.b85encode(product_secret.encode()).decode()
encoded_url = base64.b85encode(base_url.encode()).decode()

print(f"_ENCODED_PRODUCT_ID = \"{encoded_id}\"")
print(f"_ENCODED_SECRET = \"{encoded_secret}\"")
print(f"_ENCODED_URL = \"{encoded_url}\"")

# In your program, decode at runtime
def decode_config(encoded: str) -> str:
    return base64.b85decode(encoded).decode('utf-8')

PRODUCT_ID = decode_config(_ENCODED_PRODUCT_ID)
PRODUCT_SECRET = decode_config(_ENCODED_SECRET)
BASE_URL = decode_config(_ENCODED_URL)
```

## License

MIT License - see LICENSE file for details.

## Support

For support, contact: support@revshop.com