# Lizenzierung einbauen

> Kompakter Auftrag für neue Anwendungen – so bekommt eigene Software Lizenzen von Cyrellian. Zum Einfügen in den Prompt.

Diese Seite ist ein **Auftrag zum Kopieren**: Wer eine neue Anwendung baut (oder einen Coding-Agenten damit
beauftragt), fügt sie komplett in den Prompt ein. Sie fasst zusammen, was die Software tun muss, um Lizenzen von
Cyrellian zu bekommen und zu prüfen. Die Details stehen in der [Lizenzschlüssel-API](doc:lizenzschluessel-api) und
der [Lizenz-API](doc:lizenz-api).

## 1. Art wählen

| Art (Konsole) | Wann | Wie kommt die Lizenz in die Software? |
|---|---|---|
| **Nur Lizenz** (Standard) | Desktop-Apps, CLIs, Plugins, selbst gehostete Werkzeuge ohne Kundenkonto | Kunde tippt einen **Lizenzschlüssel** ein, die Software aktiviert ihn auf dem Gerät |
| **Plattform** | Selbst gehostete Produkte mit Konto-Verknüpfung, Tarifen, Updates und Add-ons (wie Plazello, Docsello) | Installation wird im Browser mit einem **Cyrellian-Konto verknüpft** |

Im Zweifel: **Nur Lizenz**. Der Rest dieser Seite beschreibt diese Art; Plattform-Anwendungen siehe
Abschnitt 7.

## 2. Einrichtung in der Konsole (Operator)

1. **Konsole → Anwendungen → Neue Anwendung**: ID (z. B. `mein-tool`, 2–40 Zeichen, Kleinbuchstaben, Ziffern,
   Bindestriche), Name, Art **Nur Lizenz**, **Features (JSON)** – die Funktionen, die Tarife freischalten, mit ihrem
   Standardwert, z. B. `{"pro": false, "maxProjects": 3, "export": ["pdf"]}` (`null` = unbegrenzt).
   Optional: „Verwaltungscode zu jedem Schlüssel“.
2. Cyrellian legt dabei den Ed25519-Signaturschlüssel und einen Tarif `standard` an. Weitere **Tarife** mit ihren
   Feature-Werten unter **Tarife**.
3. Die **Übersichtsseite** der Anwendung zeigt die **Basis-URL** und den **öffentlichen Schlüssel** – beides kommt
   fest in die Software.
4. **Lizenzschlüssel → Schlüssel ausstellen**: Tarif, Anzahl Geräte (leer = unbegrenzt), Ablauf (leer = nie), Kunde.

## 3. Was die Software als Konfiguration bekommt

| Wert | Beispiel | Hinweis |
|---|---|---|
| Basis-URL | `https://updates.cyrellian.com/mein-tool/license/v1` | Dev: `http://updates.localhost:3200/mein-tool/license/v1` |
| Öffentliche Schlüssel | `["MCowBQYDK2VwAyEA…"]` | Base64, SPKI DER. **Liste**, damit ein Schlüsselwechsel möglich ist. In den Build einbauen, nicht zur Laufzeit holen |
| Anwendungs-ID | `mein-tool` | steht im Token als `app` |

Nie in die Software: private Schlüssel, Konsolen-Zugänge, Operator-Tokens. Die Software braucht nur die öffentlichen
Daten oben.

## 4. Umsetzen – Checkliste

**Client:** Für JavaScript/TypeScript (Node 20+, Electron, Bun, Deno, Browser) die Datei
`packages/license-client/src/index.ts` aus dem Cyrellian-Repository ins Projekt kopieren (keine Abhängigkeiten,
Klasse `LicenseClient`). Andere Sprachen setzen Abschnitt 6 um.

1. **Geräte-ID (`machineId`)**: stabil über Neustarts und Updates, 4–200 Zeichen aus `A–Z a–z 0–9 . _ : -`.
   Nicht roh den Rechnernamen nehmen, sondern einen Hash, z. B. `sha256(<OS-Maschinen-ID> + ":" + <App-ID>)` als Hex.
   Bei Server-Software: eine beim ersten Start erzeugte und gespeicherte UUID.
2. **Token speichern**: Datei im App-Datenordner, Schlüsselbund oder `localStorage` (`storage.get` / `storage.set`).
   Gespeichert wird nur das **Token**, nicht der Lizenzschlüssel – `validate` und `deactivate` funktionieren mit dem
   Token.
3. **Aktivieren**: Eingabefeld für den Schlüssel (`XXXXX-XXXXX-XXXXX-XXXXX-XXXXX`, Groß-/Kleinschreibung,
   Leerzeichen und Bindestriche egal) und optional ein Gerätename → `activate(key, { name })`.
4. **Prüfen**: `check()` beim Start und etwa **einmal am Tag** (nicht öfter als stündlich). Es fragt den Server und
   fällt offline auf das gespeicherte Token zurück.
5. **Freischalten**: Funktionen ausschließlich aus `state.features` ableiten (Werte wie im Feature-Template der
   Konsole). Ohne gültige Lizenz gelten die Standardwerte des Templates bzw. der freie Umfang – nie ein lokales
   „ist Pro“-Flag, das ohne Token wahr sein kann.
6. **Anzeigen**: Tarif (`planName`), Ablauf der Lizenz (`licenseExpiresAt`, `null` = unbefristet), „offline geprüft“
   wenn `source === "offline"`, und – falls vorhanden – einen Link „Lizenz verwalten“ auf `manageUrl`.
7. **Abmelden**: Knopf „Lizenz von diesem Gerät entfernen“ → `deactivate()` (gibt den Geräteplatz frei). Auch beim
   Deinstallieren, wenn möglich.
8. **Fehler übersetzen** (Tabelle unten) – nie den rohen Code anzeigen.

```ts
import { LicenseClient, LicenseError } from "./license-client";

const license = new LicenseClient({
  baseUrl: LICENSE_API_URL,
  publicKeys: LICENSE_PUBLIC_KEYS,
  machineId: await machineId(),
  storage: { get: () => readTokenFile(), set: (t) => writeTokenFile(t) },
  version: APP_VERSION,
});

// Start und täglich
const state = await license.check();
const pro = state.valid && state.features.pro === true;

// Aktivieren aus dem Dialog
try {
  await license.activate(input, { name: os.hostname() });
} catch (e) {
  if (e instanceof LicenseError) showError(e.code);
}
```

## 5. Verhalten, das stimmen muss

| Situation | Verhalten |
|---|---|
| Server sagt `invalid_key`, `expired`, `suspended`, `revoked`, `not_activated` | Token löschen, Lizenz ist auf diesem Gerät weg (macht `check()` selbst) |
| Kein Netz, Timeout, `rate_limited`, 5xx | Gespeichertes Token entscheidet – kein Fehlerdialog |
| Token abgelaufen (7 Tage ohne erfolgreiche Prüfung) | Freier Umfang, Hinweis „Bitte online gehen, um die Lizenz zu prüfen“. Nie Daten löschen oder sperren |
| Lizenz endet (`licenseExpiresAt`) | Token läuft spätestens dann ab; vorher (z. B. 14 Tage) auf Verlängerung hinweisen |

Fehlertexte für die Oberfläche:

| Code | Meldung (Sinn) |
|---|---|
| `invalid_request` | Der Schlüssel hat nicht das richtige Format. |
| `invalid_key` | Diesen Schlüssel gibt es nicht. |
| `expired` / `suspended` / `revoked` | Die Lizenz ist abgelaufen / gesperrt / widerrufen. |
| `limit_reached` | Der Schlüssel ist schon auf allen erlaubten Geräten aktiv – erst ein Gerät abmelden. |
| `not_activated` | Dieses Gerät ist für den Schlüssel nicht mehr aktiviert – bitte neu aktivieren. |
| `rate_limited` | Zu viele Versuche – bitte kurz warten. |
| `network` | Keine Verbindung zum Lizenzserver. |
| `not_configured`, `not_found` | Fehler auf unserer Seite (falsche Basis-URL oder Anwendung nicht eingerichtet). |

Limits des Servers: 120 Anfragen pro Minute und IP, höchstens 20 unbekannte Schlüssel pro IP in 10 Minuten.

## 6. Ohne Client-Paket (andere Sprachen)

Alle Aufrufe sind JSON, ohne Cookies, CORS ist offen:

| Aufruf | Body | Antwort |
|---|---|---|
| `POST <basis>/activate` | `{key, machineId, name?, version?}` | `{status: "active", token, license}` |
| `POST <basis>/validate` | `{token, machineId, version?}` (oder `key` statt `token`) | `{status: "active", token, license}` – immer das neue Token speichern |
| `POST <basis>/deactivate` | `{token, machineId}` | `{ok: true}` |
| `GET <basis>/public-key` | – | `{algorithm: "Ed25519", keys: [{publicKey, active}]}` |

Fehler: HTTP 4xx/5xx mit `{error: "<code>"}`. `license` = `{plan, planName, features, expiresAt, tokenExpiresAt,
maxActivations, manageUrl?}`.

**Token offline prüfen** – das Token ist `<inhalt>.<signatur>`, beides base64url:

1. Am ersten `.` trennen.
2. Signatur (base64url → 64 Bytes) mit Ed25519 über die **ASCII-Bytes des Inhalt-Teils** (nicht über das
   dekodierte JSON) prüfen – gegen jeden der öffentlichen Schlüssel, einer muss passen. Der öffentliche Schlüssel ist
   Base64 SPKI DER; der rohe 32-Byte-Schlüssel sind die letzten 32 Bytes.
3. Inhalt dekodieren (base64url → JSON) und prüfen: `typ === "license_key"`, `status === "active"`,
   `sub === machineId`, `app === <App-ID>`, `exp` (Unix-Sekunden) in der Zukunft.
4. `features`, `plan`, `planName`, `licenseExpiresAt` verwenden.

## 7. Plattform-Anwendungen

Für Anwendungen der Art **Plattform** gibt es keine Lizenzschlüssel; die Installation wird mit einem Konto
verknüpft (Details: [Lizenz-API](doc:lizenz-api), Updates: [Update-API](doc:update-api)).

1. Beim ersten Start eine `instanceId` (UUID) erzeugen und speichern.
2. „Mit Konto verknüpfen“: 32 zufällige Bytes als `secret` erzeugen (nur lokal), dann
   `POST https://account.cyrellian.com/<app>/api/license/link/start` mit
   `{instanceId, name, url, version, secretHash: sha256hex(secret)}` → `{code, approveUrl, expiresAt}`.
3. `code` und `approveUrl` anzeigen; der Besitzer meldet sich dort an und wählt einen Tarif. Code gilt 30 Minuten.
4. Alle paar Sekunden `POST …/api/license/link/poll` mit `{instanceId, code, secret}` bis `status` `approved`
   (dann `license` speichern), `expired` oder `unknown`.
5. Täglich `POST …/api/license/refresh` mit `Authorization: Bearer <lizenz>` und `{version}` – neues Token
   speichern; `{status: "unlinked"}` → Lizenz löschen, freier Umfang.
6. Token offline prüfen wie in Abschnitt 6, aber `sub === instanceId`, ohne `typ`/`app`. Gültig 30 Tage, danach
   14 Tage Kulanz mit Hinweis, dann freier Umfang. Die öffentlichen Schlüssel liefert
   `GET https://updates.cyrellian.com/<app>/license/v1/public-key` (beim Verknüpfen holen und speichern).
7. „Trennen“: `POST …/api/license/unlink` mit Bearer-Token, dann Lizenz lokal löschen.
8. Updates: `GET https://updates.cyrellian.com/<app>/v1/check?channel=stable&version=<v>&instance=<instanceId>`
   mit Bearer-Token; vor dem Installieren SHA-256 und [Signatur](doc:signaturen) prüfen.

## 8. Fertig, wenn …

- ein in der lokalen Konsole ausgestellter Testschlüssel aktiviert, die Pro-Funktion freigeschaltet und nach
  `deactivate` wieder gesperrt ist,
- die Software ohne Netz mit gespeichertem Token weiterläuft und nach Ablauf des Tokens auf den freien Umfang fällt,
- ein in der Konsole gesperrter Schlüssel beim nächsten `check()` die Freischaltung entfernt,
- ein Token mit verändertem Inhalt oder fremder `machineId` abgelehnt wird,
- `limit_reached` und `invalid_key` verständliche Meldungen zeigen.
