Metadata-Version: 2.4
Name: icare-licensing
Version: 1.2.0
Summary: Secure cryptographic offline-first licensing client library for Icare Licensing Service
Author: Icare Dev Team
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.7
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.25.0
Requires-Dist: cryptography>=3.0.0

# Icare Licensing Client

`icare-liecensing-client` is an offline-first, cryptographically secure licensing client library for Python applications. It integrates seamlessly with the Icare licensing server using RSA-SHA256 signatures, device hardware bindings, and automated offline-resilient sync policies.

---

## Features
- **Stable Hardware Binding (HWID)**: Generates a stable machine signature.
- **Offline Cryptographic Validation**: Validates license state offline using RSA-SHA256.
- **Automated Sync Daemon**: Runs in the background and pings the verification endpoint every 24 hours.
- **3-Day Offline Grace Period**: Remains fully functional offline for up to 3 days before requiring internet.
- **Instant Revocation Purge**: Deletes local signatures and metadata immediately upon server-side license revocation or deletion to prevent offline bypass.

---

## 1. Installation

Install the package from your PyPI registry:
```bash
pip install icare-licensing
```

---

## 2. License Storage & File Location Policy

By default, the client library writes `license.lic` and `license_metadata.json` relative to the **Current Working Directory (CWD)** of the executing process.

### Critical Production Guidelines
For desktop deployments or system-wide CLI tools, running relative to the CWD can lead to:
1. **Permission Denials (`PermissionError`)**: If the app is run from a read-only directory (e.g. `Program Files` or `/usr/local/bin`).
2. **Missing Licenses**: If the user starts the application from a different terminal directory, the client will look in the wrong path and report the license is missing.

### Recommendation: Use Absolute Paths
Always initialize the client with an **absolute path** pointing to a user-writable application directory (such as the user's home folder or standard application data directory).

```python
import os
from icare_licensing import LicensingClient

# Determine a safe system path (e.g., ~/.config/my_app/ or %APPDATA%/my_app/)
app_folder = os.path.join(os.path.expanduser("~"), ".my_app")
os.makedirs(app_folder, exist_ok=True)

license_path = os.path.join(app_folder, "license.lic")

# Initialize client
client = LicensingClient(
    server_url="https://licensing.mycompany.com",
    license_file_path=license_path
)
```
*Note: The metadata file (`license_metadata.json`) will automatically be generated in the same target folder.*

---

## 3. Core Developer API

### Licensing States
The library exposes six core status constants:
* `STATUS_VALID`: License signature is cryptographically valid, matches machine HWID, is not expired, and is synced.
* `STATUS_MISSING`: No `license.lic` file is present (Device needs activation).
* `STATUS_EXPIRED`: The local or server expiration date has been exceeded.
* `STATUS_INVALID`: The license cryptographic signature is corrupt, has been modified, or has been actively revoked/deleted on the server.
* `STATUS_REQUIRES_SYNC`: The application has been running offline for more than 3 days and requires internet to refresh.
* `STATUS_UNKNOWN`: State is not yet resolved.

---

## 4. Integration Template

Copy this robust integration template into your project's main entry point:

```python
import os
import sys
from icare_licensing import (
    LicensingClient,
    STATUS_VALID,
    STATUS_REQUIRES_SYNC,
    STATUS_MISSING,
    STATUS_INVALID,
    STATUS_EXPIRED
)

def on_license_status_change(new_status):
    """
    Callback triggered when the license status transitions.
    NOTE: If running a GUI application (e.g., Tkinter/PyQt), make sure 
    to route UI-altering updates safely to the main thread.
    """
    print(f"[Licensing] Status changed to: {new_status}")
    
    if new_status == STATUS_VALID:
        print("Premium features unlocked!")
        # Proceed to launch normal application logic
        
    elif new_status == STATUS_REQUIRES_SYNC:
        print("Warning: internet connection required to verify license.")
        # Optional: Show a warning banner, but keep standard features unlocked
        
    elif new_status in (STATUS_MISSING, STATUS_INVALID, STATUS_EXPIRED):
        print("Security Lock: No valid license active.")
        # Action: Redirect user to your license activation overlay/screen
        # Lock down core software capabilities

def main():
    # 1. Setup secure storage paths
    app_folder = os.path.join(os.path.expanduser("~"), ".my_app")
    os.makedirs(app_folder, exist_ok=True)
    license_path = os.path.join(app_folder, "license.lic")

    # 2. Instantiate Client
    # max_offline_days determines how long the app can run offline (default: 3 days)
    client = LicensingClient(
        server_url="http://localhost:8081",
        license_file_path=license_path,
        status_callback=on_license_status_change,
        max_offline_days=3
    )

    # 3. Handle Initial State
    # Force a check immediately on startup
    client._run_check()

    if client.current_status == STATUS_MISSING:
        print("Application is unactivated.")
        # Example: prompt user for activation key in CLI
        # key = input("Enter Activation Key: ")
        # success, msg = client.activate_device(key)

    # 4. Start Background Protection Thread
    # Validates locally and checks online every 5 minutes (300 seconds)
    client.start_monitoring(interval_seconds=300)

if __name__ == "__main__":
    main()
```
