# Plugin entitlements and activation (SDK 1.0)

Package authenticity and purchase rights are separate. A `.etplugin` is accepted
only when its Ed25519 publisher key is trusted and its inventory verifies. A
paid product additionally needs a signed entitlement from a separately trusted
issuer. The host contains no private keys and trusts no test issuer by default.

## Service contract

Request `ET_SERVICE_LICENSE` v1 with an `et_license_service` table from
`<epictuner/license.h>`. It uses the same bounded async request/reply structure
as the read services. All calls run on the worker dispatcher; callbacks arrive
on that dispatcher and are cancelled during stop. Operations:

| Operation | Reply |
|---|---|
| `ET_LICENSE_STATUS` | `reply.text` is compact JSON with `state`, `allowed`, `reason`, and `features`. `request.id` may name one feature. |
| `ET_LICENSE_CHALLENGE` | `reply.text` is the current offline challenge JSON. A new challenge replaces the previous one. |
| `ET_LICENSE_ACTIVATE` | Opens the host Plugins window so the user can complete online or offline activation. |
| `ET_LICENSE_DEACTIVATE` | Clears the cached local entitlement. |

For the `licensed-feature-demo`, `ET_LICENSE_STATUS` checks
`org.epictuner.feature.pro` before running the paid action. Host tune proposal,
apply, undo, redo and Burn requests from a paid plugin are checked again at
dispatch; completed host operations are not undone when a license expires.
Read-only work and panel close/export remain available. Plugin authors must
check their own protected algorithms at the point of use as well.

## Signed response

The issuer signs canonical UTF-8 JSON entitlement bytes with Ed25519 over
`EpicTunerEntitlement/v1\n` followed by the canonical entitlement. The response
contains the original challenge, entitlement and detached signature. The
entitlement binds issuer/key ID, product/plugin, license ID, policy, features,
version range, installation ID, challenge nonce, issue time, expiry and lease.
Only the public issuer key is imported into the host. The issuer private key
remains outside the SDK, host, package and cache.

The plugin manager supports HTTPS activation endpoints and a loopback HTTP
exception for the local test issuer. Offline activation exports a challenge
JSON file and imports a response JSON file signed for that challenge. A
response for another product, installation, nonce or issuer is rejected.
Each new activation requires a new challenge. The manager reports failures
without unloading the plugin or replaying work.

## Policy

- **Free:** no entitlement or network access.
- **Trial:** signed expiry; no offline extension after expiry.
- **Perpetual:** version-bounded, available offline indefinitely while the
  issuer key remains trusted and the installation ID matches.
- **Subscription:** signed expiry is a hard stop. A signed `leaseUntil` permits
  72 hours of offline grace before new restricted work is blocked. The issuer
  must renew the lease through a new activation before grace ends.

The host rejects a clock more than five minutes behind its last observed time.
That local check cannot prevent a user who controls their machine from changing
state or rolling back storage. Revoked issuer keys block cached receipts on the
next status check; offline clients cannot learn server-side revocations until
they reconnect or their signed lease/grace ends. Publishers must define refund,
transfer, activation-limit and account-recovery policy in their issuer service.

Windows caches the signed response with DPAPI for the current user. Linux stores
the signed response in a `0600` per-user file. Neither format is a claim of
tamper-proof DRM; user-controlled native code and local account access remain
outside the plugin boundary.

## Local test issuer

`tools/test-issuer.py` is deliberately local and uses an **external** Ed25519
PEM. Test keys are not shipped in the SDK or application. Example:

```sh
python tools/test-issuer.py issue --key /outside/sdk/issuer.pem \
  --issuer org.example.test --key-id org.example.test.key1 \
  --policy subscription --feature org.epictuner.feature.pro \
  --expires 1790200000 --lease 1790120000 \
  --challenge challenge.json --output response.json
```

For online mock activation, replace `issue` with `serve --port 8765` and point
the Plugins window to `http://127.0.0.1:8765/activate`. Production activation
requires an independently deployed HTTPS issuer, revocation and recovery
operations. The mock proves the host/SDK mechanics only.
