# Licence integration guide How to add LeTissier licence checking to an app. Written to be followed directly — by a person or by an agent working in one of the app repositories. **Service base URL:** `https://letissier.ie` **Signing public key (embed this):** `REPLACE_WITH_YOUR_PUBLIC_KEY_HEX` The public key is safe to ship inside a binary. It only verifies; it cannot mint a licence. ## Which apps use this | App | Repository | Language | SDK to copy | | --- | --- | --- | --- | | Vizz | `legofsalmon/vizz` | Rust | `https://letissier.ie/integrate/sdk/rust/licence.rs` | | Light | `legofsalmon/light` | Rust | `https://letissier.ie/integrate/sdk/rust/licence.rs` | | Datamosh | `legofsalmon/ffgl-datamosh` | C++17 | `https://letissier.ie/integrate/sdk/cpp/licence.hpp` + `licence.cpp` | | Yewee | `legofsalmon/facetrack` | Python | `https://letissier.ie/integrate/sdk/python/letissier_licence.py` | | Crewbox | `legofsalmon/crewbox` | TypeScript | `https://letissier.ie/integrate/sdk/node/licence.ts` | Product ids used by the API: `vizz`, `light`, `datamosh`, `yewee`, `crewbox`. A machine-readable index of everything on this page is at `https://letissier.ie/integrate/manifest.json`, and this guide in plain text is at `https://letissier.ie/integrate/llms.txt`. ## How the model works A licence is an **Ed25519-signed claim blob**: `base64url(payload).base64url(signature)`. The app embeds the public key above and verifies it **offline**. There is no network call in the hot path, and no secret in the binary. Two dates in the claims do different jobs, and confusing them is the main way an integration goes wrong. **`maintUntil` — what the customer is entitled to.** Compare it against **the build's own release date**, compiled in. A build released at or before `maintUntil` runs forever. A build released after it is outside the customer's update window. This is what makes "one year of updates, yours to keep" work with no server: nothing expires, so a machine that never reconnects keeps running the version it is entitled to. **`exp` — the check-in deadline for the current lease.** Passing it does *not* end the licence. The app checks in and receives a fresh token with a new `exp`. Default is 30 days; it is set per licence, so a tour licence may be 90 or 180 days without any code change. ### Claims ```json { "v": 1, "key": "LT-V1ZZ-K7M2-9PQR-4XTC", "product": "vizz", "edition": "standard", "customer": "uuid", "name": "Buyer Name", "seats": 2, "maintUntil": 1791536000, "exp": 1762505600, "machine": "8b9dd6da2bcf47bdfe7ceb27c2a58680", "mode": "online", "iat": 1760000000, "jti": "uuid" } ``` Reject any token whose `v` is not `1`. ### Statuses `check()` returns exactly one of these. Handle all six. | Status | Meaning | What the app should do | | --- | --- | --- | | `active` | Everything is in order | Run normally | | `update_required` | Licence valid, but this build is newer than `maintUntil` | Run an entitled build, or prompt to renew. Do not treat as piracy | | `check_in_required` | The lease lapsed | Try a heartbeat. Apply your own grace period before restricting | | `expired` | A trial that ran out | Prompt to buy | | `wrong_machine` | Token was issued for another machine | Re-activate | | `invalid` | Bad signature, malformed, or unknown version | Treat as unlicensed | Fail toward the customer, not against them. A rig mid-show should warn loudly and keep running rather than stop dead. ## Machine fingerprint Produce a stable per-machine string. The SDK hashes it before it leaves the machine — `sha256(trimmed_fingerprint)` hex, **first 32 characters** — and the service only ever stores that hash. | Platform | Source | | --- | --- | | macOS | `ioreg -rd1 -c IOPlatformExpertDevice \| awk -F'"' '/IOPlatformUUID/{print $4}'` | | Linux | `/etc/machine-id` | | Windows | `HKLM\SOFTWARE\Microsoft\Cryptography` → `MachineGuid` | Do not use a MAC address: they change with docks, VPNs and USB adapters, which would burn a seat every time a customer plugs in a dongle. ## API All endpoints are `POST`, take and return JSON, and allow cross-origin use. Errors return `{ "ok": false, "reason": "...", "message": "..." }`. ### Activate a machine ``` POST https://letissier.ie/api/licence/activate { "key": "LT-V1ZZ-...", "machine": "", "label": "FOH laptop" } ``` ```json { "ok": true, "token": "eyJ2Ijox....abc", "product": "vizz", "edition": "standard", "seats": 2, "seatsUsed": 1, "checkInBy": "2026-09-18T00:00:00.000Z", "maintenanceUntil": "2027-08-21T00:00:00.000Z" } ``` Activating the same machine twice renews its lease rather than consuming a second seat. ### Check in ``` POST https://letissier.ie/api/licence/heartbeat { "key": "LT-V1ZZ-...", "machine": "" } ``` Returns a fresh `token` and `checkInBy`. Call on launch and roughly daily. **Persist the returned token** — that is what resets the offline window. ### Release a seat ``` POST https://letissier.ie/api/licence/deactivate { "key": "LT-V1ZZ-...", "machine": "" } ``` ### Start a trial ``` POST https://letissier.ie/api/licence/trial { "product": "vizz", "email": "vj@example.com", "machine": "", "name": "VJ" } ``` Issues and activates a 30-day trial in one call, returning `key`, `token` and `expiresAt`. One trial per machine per product. ### Error reasons | Reason | HTTP | Meaning | | --- | --- | --- | | `bad_request` | 400 | Required field missing | | `malformed_key` | 400 | Key failed its checksum — likely a typo | | `bad_email` | 400 | Email did not parse | | `unknown_key` | 404 | No such licence | | `not_activated` | 404 | Heartbeat from a machine that never activated | | `revoked` | 403 | Licence revoked, usually a refund | | `expired` | 403 | Trial ended | | `no_seats` | 409 | All seats in use; release one first | | `trial_already_used` | 409 | This machine already had a trial | | `server_error` | 500 | Retry with backoff | Treat a network failure as **not** a licensing failure: fall back to the cached token and its `exp`. Never block launch because the service was unreachable. ## Integration steps The same five steps in every language. 1. Copy the SDK file(s) into the app. 2. Replace `REPLACE_WITH_YOUR_PUBLIC_KEY_HEX` with the key at the top of this page. 3. Compile in the build's release date as a Unix timestamp. Bake it in — if it is read from the filesystem, anyone can edit it. 4. On launch: load the cached token, call `check`, act on the status. 5. In the background: heartbeat when online and persist the returned token. Store the token wherever the app already keeps user state. It is not a secret — it is signed, and useless on another machine. ### Rust — Vizz, Light Add to `Cargo.toml`: ```toml [dependencies] ed25519-dalek = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" sha2 = "0.10" base64 = "0.22" hex = "0.4" ``` ```rust use letissier_licence::{check, Status}; const BUILD_DATE: i64 = 1_761_000_000; // set from CI at build time const PUBLIC_KEY: &str = "REPLACE_WITH_YOUR_PUBLIC_KEY_HEX"; let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH)? .as_secs() as i64; match check(&token, &fingerprint, BUILD_DATE, now, PUBLIC_KEY).status { Status::Active => start(), Status::UpdateRequired => warn_update_window_ended(), Status::CheckInRequired => try_heartbeat_then_grace(), Status::Expired => prompt_purchase(), Status::WrongMachine => prompt_reactivate(), Status::Invalid => prompt_licence_entry(), } ``` ### C++ — Datamosh Pick a crypto backend at compile time: ```sh c++ -std=c++17 -DLICENCE_BACKEND_SODIUM licence.cpp -lsodium # for shipped plugins c++ -std=c++17 licence.cpp -lcrypto # OpenSSL ``` Use **libsodium** for the FFGL plugin: it links statically, so the plugin has no runtime dependency for a customer to install. ```cpp #include "licence.hpp" constexpr std::int64_t kBuildDate = 1761000000; const auto verdict = letissier::check(token, fingerprint, kBuildDate, now); if (verdict.status == letissier::Status::Active) { enable_plugin(); } else { show_status(letissier::to_string(verdict.status)); } ``` A plugin cannot show a dialog mid-render. Check once at load, cache the verdict, and surface the state as a parameter or a watermark rather than blocking the render thread. ### Python — Yewee Requires `cryptography` (add to `requirements.txt`). ```python from letissier_licence import check, ACTIVE, CHECK_IN_REQUIRED BUILD_DATE = 1761000000 verdict = check(token, fingerprint, BUILD_DATE) if verdict.status == ACTIVE: start_tracking() elif verdict.status == CHECK_IN_REQUIRED: try_heartbeat() else: show_licence_panel(verdict.status) ``` ### TypeScript — Crewbox No dependencies; Node's `crypto` has Ed25519 built in. ```ts import { check, LicenceClient } from "./licence"; const BUILD_DATE = 1761000000; const verdict = check({ token, fingerprint, buildDate: BUILD_DATE }); if (verdict.status !== "active") { const client = new LicenceClient({ fingerprint }); const fresh = await client.heartbeat(key).catch(() => null); if (fresh) persist(fresh.token); } ``` Crewbox runs offline by design, so its check must never gate startup on the network. Verify the cached token, and treat an unreachable service as normal. ## Verifying an integration The SDKs are proven to agree with each other and with the server signer. Test vectors are published at `https://letissier.ie/integrate/vectors.json` — signed with a **test** key included in that file, not the production key above. A correct integration reproduces these, using the vectors' own `publicKeyHex`: | Input | Expected | | --- | --- | | `tokens.valid` | verifies | | `tokens.tampered` | fails — payload edited after signing | | `tokens.wrongKey` | fails — signed by an untrusted key | | `tokens.malformed` | fails | | `fingerprint` hashed | equals `machineHash` | | build date after `maintUntil` | `update_required` | | `now` after `exp`, standard licence | `check_in_required` | | `now` after `exp`, trial | `expired` | If your implementation disagrees on any row, it is wrong — the four shipped SDKs all agree on all of them. ## Rules - Never send the raw fingerprint anywhere but this service, and never log it. - Never ship the private signing key. Only the public key belongs in an app. - Do not invent extra claim fields; the signature covers the exact payload. - Do not fail closed on a network error. - Do not use `maintUntil` as an expiry date. It is an entitlement boundary — treating it as an expiry would switch off software people have paid to keep.