Metadata-Version: 2.5
Name: osal-license
Version: 0.1.0.dev4
Summary: Licensing client for Office Solution AI Labs products
Author: Office Solution AI Labs
License: Proprietary
Keywords: entitlements,fastapi,license-key,licensing
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: License :: Other/Proprietary License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: pyjwt[crypto]>=2.9
Provides-Extra: dev
Requires-Dist: fastapi>=0.100; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.100; extra == 'fastapi'
Description-Content-Type: text/markdown

# osal-license

> **Development release.** `0.1.0.dev*` builds trust a development signing key and must not be
> shipped to customers. Use `0.1.0` or later, which trusts the production key.

Licensing client shared by every Office Solution AI Labs product. A product creates
one `License` and uses it to gate features and meter usage; everything else is handled here.

It talks to the OSAL license server and only accepts leases signed by keys built into the package.

## API

```python
from osal_license import License

license = License.from_env("PULSE-CONVERT")   # OSAL_LICENSE_KEY, OSAL_LICENSE_SERVER_URL, ...
license.start()                               # activate + background refresh; never raises for outages

license.status()            # LicenseStatus(state=active|grace|invalid, reason, lease, ...)
license.is_valid()
license.require()           # -> LeaseInfo, or raises LicenseInvalid(code=...)
license.features            # frozenset of licensed feature codes
license.has_feature("TABLEAU")
license.require_feature("TABLEAU")            # raises FeatureNotLicensed
license.limit("dashboards") # None = unlimited
license.consume("dashboards", 1, feature="TABLEAU", event_id="job:42")   # -> UsageReceipt

@license.feature("COGNOS")                    # decorators: sync or async functions
@license.consumes("dashboards", feature="COGNOS")
def convert(...): ...

license.add_listener(lambda status: ...)      # state changes, e.g. for alerts
license.stop()
```

FastAPI:

```python
from osal_license.fastapi import install_license, requires_feature, requires_license

install_license(app, license)   # middleware: 403 on every route while unlicensed; adds GET /license/status
@app.post("/x", dependencies=[requires_feature(license, "TABLEAU")])
```

Errors map to HTTP: `LicenseInvalid`, `FeatureNotLicensed`, `UsageLimitExceeded` → 403,
`LicenseServerUnavailable` → 503. Body: `{"error": <code>, "message": ...}`.

## Behaviour

| Situation | Result |
|---|---|
| Valid lease | `active` |
| Server unreachable, lease expired, within `grace_hours` | `grace` (still valid; logged) |
| Grace used up, or license past its expiry | `invalid` |
| Server says revoked / suspended / expired / blocked | `invalid` immediately; re-checked every 15 min so re-enabling recovers it |
| `consume()` while the server is unreachable | raises `LicenseServerUnavailable`: usage is never granted offline |
| Restart with the server down | runs on the cached lease (state file) |
| System clock wound back (while running or across restarts) | ignored: a trusted clock never goes backwards |
| Lease or receipt not signed by a trusted key, older than the current one, or for another installation | rejected |
| Server rotated installation secrets | re-activates automatically, same installation |

## Configuration

| Variable | Required | Notes |
|---|---|---|
| `OSAL_LICENSE_KEY` | yes | |
| `OSAL_LICENSE_SERVER_URL` | yes | |
| `OSAL_STATE_DIR` | no | Default `/home/osal-license` on App Service, else `~/.osal-license` |
| `OSAL_APP_VERSION` | no | Reported to the server |
| `OSAL_AZURE_TOKEN_AUDIENCE` | no | Send a managed-identity token so the server can verify the Azure environment |
| `OSAL_MANAGED_IDENTITY_CLIENT_ID` | no | For user-assigned identities |
| `OSAL_RESOURCE_ID`, `OSAL_TENANT_ID` | no | Override environment detection (AKS, VMs) |

**Trusted signing keys are compiled in** (`trusted_keys.py`) and cannot be set from the
environment. Otherwise a customer could point the product at their own key and sign their own leases.

## Limits of client-side protection

A customer with root access to the container can, with enough effort, patch checks out
of the binary or edit the state file. This package makes that expensive (compiled
code + many gates + signed leases + server-side usage) and the server bounds the damage
(leases expire within a day, plus grace). Keep the highest-value logic server-side if it must be unbreakable.

## Install

```bash
pip install "osal-license[fastapi]"
```
