# IF-Net License Client

SDK à embarquer dans toute application vendue sous licence IF-Net. Il ne dépend
d'aucun framework : PHP 8.2, `ext-sodium`, `ext-json`, et c'est tout.

## Principe

Le serveur de licences ne valide pas l'application à chaque requête. Il délivre
un **jeton signé en Ed25519**, à durée de vie courte, que l'application vérifie
elle-même hors ligne. Trois conséquences pratiques :

- une panne du serveur de licences n'arrête pas les sites des clients ;
- la vérification à chaque requête coûte une vérification de signature, pas un appel réseau ;
- une révocation prend effet au plus tard à l'expiration du jeton, soit 72 heures par défaut.

## Installation

```bash
composer config repositories.ifnet-license path ../if-panel/packages/license-client
composer require ifnet/license-client
```

## Mise en service

```php
use IfNet\License\LicenseClient;
use IfNet\License\Storage\FileStorage;

$license = new LicenseClient([
    'endpoint'    => 'https://clients.if-net.fr/api/v1/license',
    'product'     => 'mon-application',
    'public_key'  => 'ifpanel-v1:BASE64_DE_LA_CLE_PUBLIQUE',
    'storage'     => new FileStorage(__DIR__.'/../storage/license.json'),
    'app_version' => '1.4.2',
]);
```

La clé publique se récupère dans l'administration du portail, sur la fiche du
produit sous licence. Elle n'est pas secrète : elle ne permet que de vérifier,
jamais de signer.

## Les trois appels

```php
// Une seule fois, quand l'administrateur saisit sa clé.
$license->activate('IFPN-7K3M-9QXD-2VBH-5RTZ');

// À chaque requête. Aucun accès réseau, aucun accès disque au-delà du jeton.
$status = $license->status();

// Dans une tâche planifiée quotidienne, jamais dans le cycle d'une requête web.
$license->refreshIfDue();
```

## Exploiter le verdict

```php
$status = $license->status();

if (! $status->isUsable()) {
    return redirect()->route('licence.invalide')->with('message', $status->message());
}

if ($status->needsAttention()) {
    // Encore utilisable, mais il faut prévenir : période de grâce en cours.
    session()->flash('warning', $status->message());
}

if ($status->hasFeature('api')) {
    // Débloque le module optionnel.
}

if (! $status->withinLimit('max_customers', Customer::count())) {
    // Le plan souscrit est dépassé, proposer une montée de gamme.
}
```

## États possibles

| État | Sens | L'application doit |
|---|---|---|
| `valid` | Jeton signé, à jour, conforme au domaine et à l'installation | Fonctionner normalement |
| `grace` | Jeton périmé mais dans le délai de tolérance | Fonctionner, avec un avertissement visible |
| `expired` | Délai de tolérance dépassé, ou licence arrivée à son terme | Se restreindre |
| `invalid` | Signature fausse, mauvais domaine, mauvaise installation | Se restreindre |
| `unlicensed` | Aucune activation faite | Afficher l'écran de saisie de clé |

## Ce que le jeton empêche

Copier un site licencié vers un autre domaine ne suffit pas : le domaine est
signé dans le jeton et revérifié localement. Restaurer une sauvegarde sur une
autre machine ne suffit pas non plus, l'empreinte d'installation est signée de
la même façon. Modifier la charge du jeton pour s'offrir une fonctionnalité
invalide la signature.

Ce que le jeton n'empêche pas, et aucun système de ce type ne l'empêche : un
client qui modifie le code source de l'application pour retirer l'appel à
`status()`. La protection vise l'usage non intentionnel et la revente, pas un
attaquant déterminé disposant du code.

## Tâche planifiée

```php
// Laravel
Schedule::call(fn () => $license->refreshIfDue())->dailyAt('03:17');
```

```cron
# Hors Laravel
17 3 * * * /usr/bin/php /chemin/vers/bin/license-refresh.php
```

L'heure décalée évite que toutes les installations appellent le serveur à la
même minute.
