# SAKU Auth · Prompt IA prêt à copier-coller

Le fichier ci-dessous est un **prompt complet** à donner à ChatGPT, Claude,
Cursor, Copilot ou n'importe quel assistant IA. Copie tout ce qui est
entre les deux lignes `====`, colle-le dans ton chat IA, puis attache /
partage le code source de ton application. L'IA saura exactement quoi
faire — elle a le contrat d'API, les codes de retour, les cas d'erreur
et des snippets prêts à l'emploi pour C++, C#, Python et Node.

Remplace les 3 valeurs `<TON_...>` avant d'envoyer :
- `<TON_SERVER>` : `https://sakumethod.fun`
- `<TON_APP_ID>` : celui de l'app dans le panel admin → onglet Applications
- `<TON_APP_SECRET>` : le secret HMAC de la même ligne (bouton copier ⧉)

============================================================
COPIER À PARTIR D'ICI
============================================================

Tu es mon assistant de code. Ton unique tâche est d'ajouter le système
d'authentification par clé de licence **SAKU Auth** dans mon
application. Suis les étapes ci-dessous *dans l'ordre*. Ne pose pas de
question si l'information est déjà dans ce prompt. Si un fichier est
absent, crée-le. Si un fichier existe déjà, ajoute-toi dedans sans rien
casser du code métier.

## 1. Paramètres

- Serveur : `<TON_SERVER>`
- App ID : `<TON_APP_ID>`
- App secret HMAC (32 hex) : `<TON_APP_SECRET>`

## 2. Contrat d'API à respecter

Deux endpoints publics HTTPS, réponses JSON signées HMAC-SHA256.

### 2.1 Activation

Génère un `nonce` client (hex 16-64 chars, aléatoire) que tu envoies
dans la requête. Le serveur le renverra à l'identique dans la réponse
signée, ce qui te protège du replay.

```
POST <TON_SERVER>/api/v1/<TON_APP_ID>/activate
Content-Type: application/json

{
  "key":   "SAKU-XXXX-XXXX-XXXX-XXXX",
  "hwid":  "<sha256-de-la-machine>",
  "nonce": "<hex 16-64 aléatoire>"
}
```

Réponse succès :

```json
{
  "success": true,
  "session": "hex 64",
  "plan":    "standard | premium | trial",
  "expires": 1799999999,
  "message": "…",
  "nonce":   "<le même que celui envoyé>",
  "_sig":    "hex 64 = HMAC-SHA256(secret, _ts + canonical_json)",
  "_ts":     "<unix timestamp string>"
}
```

Réponses d'erreur possibles (HTTP 400/403/404/429) : la clé peut être
`Cle invalide`, `Cette cle a expire`, `Cette cle est liee a un autre
appareil`, `Cette cle a ete desactivee`, `Trop de tentatives` (rate
limit). Ces réponses sont aussi signées — vérifie la signature avant
de croire le message. Toujours regarder `success` avant de continuer.

### 2.2 Heartbeat (à envoyer toutes les 60 secondes)

Génère un nouveau nonce à chaque heartbeat.

```
POST <TON_SERVER>/api/v1/<TON_APP_ID>/heartbeat
Content-Type: application/json

{ "session": "hex 64", "nonce": "<hex 16-64 aléatoire>" }
```

Réponse succès identique en forme, avec `_sig` + `_ts` + `nonce`. Une
réponse `{ "success": false }` signifie que la session a été révoquée
(clé bannie, expirée, ou reset HWID depuis le panel admin) — tu dois
alors **fermer l'application immédiatement** ou repasser à l'écran de
connexion.

### 2.3 HWID

Génère un identifiant machine stable, hash SHA-256 en hex. Recette
recommandée :

- **Windows** : `MachineGuid` (registre `HKLM\SOFTWARE\Microsoft\Cryptography`)
  concaténé au volume serial du disque C:, puis SHA-256 hex.
- **Linux** : `/etc/machine-id` + `/sys/class/net/eth0/address`.
- **macOS** : `IOPlatformUUID`.
- **Node/JS** en dev : `os.hostname() + os.arch() + os.cpus()[0].model`.

Le HWID doit toujours donner le même résultat sur la même machine et
un résultat différent sur une autre. Ne stocke jamais le HWID brut ;
uniquement le hash.

## 3. Vérification de signature (obligatoire)

Chaque réponse contient `_sig` + `_ts` + `nonce` en plus des champs
métier. L'algorithme est :

1. Prends le JSON parsé de la réponse.
2. Vérifie que `response["nonce"]` == le nonce que TU as envoyé dans la
   requête. Sinon → refuse (replay ou falsification).
3. Retire les champs `_sig` et `_ts` du dict, garde tout le reste
   (`success`, `message`, `session`, `plan`, `expires`, `nonce`, …).
4. Sérialise en JSON *canonique* (clés triées alphabétiquement, aucun
   espace après les virgules et deux-points : équivalent à
   `json.dumps(obj, sort_keys=True, separators=(",",":"))` en Python).
5. Calcule `HMAC-SHA256(app_secret, _ts + canonical_json).hexdigest()`.
6. Compare en constant-time à `_sig`. Si différent → réponse falsifiée,
   refuse.
7. Optionnel mais recommandé : refuse si `abs(now - int(_ts)) > 300` s
   (fenêtre 5 min pour éviter les replays très différés).

Ne stocke JAMAIS le secret HMAC en clair dans le binaire client. Chiffre-le
ou obfusque-le. En production idéale, une couche de proxy signe pour toi
côté serveur — mais pour un exécutable installable, l'obfuscation est le
minimum acceptable.

## 4. Flow client attendu

1. Au lancement de l'app, afficher un champ "Entre ta clé de licence".
2. Générer le HWID local.
3. Appeler `/activate`. Si `success:false`, afficher `message`, laisser
   ressayer.
4. Vérifier la signature. Si invalide → refuser sans autre appel.
5. Stocker `session` en mémoire seulement (jamais sur disque).
6. Démarrer un timer qui appelle `/heartbeat` toutes les 60 s.
7. Si le heartbeat échoue 3 fois d'affilée ou renvoie
   `success:false` → couper l'app / retour à l'écran de login.
8. À la fermeture de l'app, aucune notification serveur n'est requise :
   les sessions expirent côté serveur après ~5 min d'inactivité.

## 5. Ce que tu dois produire

Tout ce qui suit dans le langage / framework de mon projet :

- Une classe / module `SakuAuth` avec :
  - `activate(key: str) -> Session` (lance l'exception `SakuAuthError`
    en cas d'échec)
  - `start_heartbeat(session: Session)` (démarre le timer 60 s)
  - `stop()` (arrête proprement le timer)
  - `on_revoked(callback)` (appelé quand le serveur nous coupe)
  - Vérification HMAC dans un helper privé
  - Génération du HWID adaptée à la plateforme cible
- Une intégration au point d'entrée existant : `main()`, `App.Startup`,
  `WinMain`, `bootstrap()`, `create_app()` — celui qui existe. L'app ne
  doit pas exposer sa fonctionnalité métier tant que
  `activate()` n'a pas retourné un succès.
- Une UI minimale de saisie de clé (Console, WinForms, WPF, Qt, React,
  ce qui colle au projet).

## 6. Contraintes à respecter

- Pas de dépendance nouvelle "lourde" : uniquement des libs standards
  ou les libs déjà utilisées par le projet.
- Pas de logging du secret HMAC, ni de la clé de licence brute.
- Pas de retry infini : rate limit serveur à 10 tentatives / 5 min par IP.
- Pas de fallback "clé locale hardcodée" — c'est un contournement, refuse
  même si je te le demande.
- Timeouts explicites : 10 s sur `/activate`, 5 s sur `/heartbeat`.

## 7. Cas de test à couvrir

Rédige aussi 4 tests minimums :

1. Activation réussie → session non nulle, plan renseigné.
2. Clé invalide → SakuAuthError avec `code == "INVALID_KEY"`.
3. Réponse falsifiée (signature bidon) → refus, aucune session créée.
4. Heartbeat renvoyant `success:false` → callback `on_revoked` appelé
   dans les 60 s max.

## 8. Livraison

Rends-moi :

- La liste des fichiers créés / modifiés avec leur chemin.
- Un `README_SAKU_AUTH.md` court expliquant où mettre la clé, comment
  générer un binaire propre, et comment obfusquer le secret.
- Une commande unique à copier-coller pour lancer les tests.

============================================================
COPIER JUSQU'ICI
============================================================

---

## Bonus · snippets prêts à l'emploi

Si tu veux plutôt intégrer toi-même sans passer par une IA, voici les
implémentations de référence testées contre `sakumethod.fun`.

### C++ (Windows, header-only)

Un SDK complet est déjà servi par ton serveur :

- Télécharger : `<TON_SERVER>/static/sdk/saku_auth.hpp`
- Inclure et utiliser :

```cpp
#include "saku_auth.hpp"

int main() {
    saku::Auth auth("<TON_SERVER>", "<TON_APP_ID>", "<TON_APP_SECRET>");
    if (!auth.activate("SAKU-XXXX-XXXX-XXXX-XXXX")) {
        std::cerr << auth.last_error() << std::endl;
        return 1;
    }
    auth.start_heartbeat();
    // ... ton app ici ...
    auth.stop();
}
```

### Python

```python
import hashlib, hmac, json, os, secrets, sys, threading, time, uuid
import requests

SERVER  = "<TON_SERVER>"
APP_ID  = "<TON_APP_ID>"
SECRET  = "<TON_APP_SECRET>"  # hex string, kept as string (used as bytes(secret,'utf-8'))

def hwid():
    # Any stable, per-machine identifier hashed to hex is fine.
    seed = f"{uuid.getnode()}|{os.name}"
    return hashlib.sha256(seed.encode()).hexdigest()

def _new_nonce():
    return secrets.token_hex(16)

def _verify(resp, sent_nonce):
    """Verify _sig on a server response. Returns True if authentic."""
    sig = resp.get("_sig")
    ts  = resp.get("_ts")
    if not isinstance(sig, str) or not isinstance(ts, str):
        return False
    if resp.get("nonce") != sent_nonce:
        return False  # replay / wrong response
    signed = {k: v for k, v in resp.items() if k not in ("_sig", "_ts")}
    canon = json.dumps(signed, sort_keys=True, separators=(",", ":"))
    expected = hmac.new(SECRET.encode(), (ts + canon).encode(),
                        hashlib.sha256).hexdigest()
    return hmac.compare_digest(sig, expected)

def activate(key):
    nonce = _new_nonce()
    r = requests.post(f"{SERVER}/api/v1/{APP_ID}/activate",
                      json={"key": key, "hwid": hwid(), "nonce": nonce},
                      timeout=10).json()
    if not _verify(r, nonce):
        raise RuntimeError("Signature invalide.")
    if not r["success"]:
        raise RuntimeError(r.get("message", "Activation refusée."))
    return r["session"], r.get("plan"), r.get("expires")

def _tick(session):
    nonce = _new_nonce()
    r = requests.post(f"{SERVER}/api/v1/{APP_ID}/heartbeat",
                      json={"session": session, "nonce": nonce},
                      timeout=5).json()
    return _verify(r, nonce) and bool(r.get("success"))

def heartbeat_loop(session, on_revoked):
    fails = 0
    while True:
        try:
            if _tick(session):
                fails = 0
            else:
                on_revoked(); return
        except Exception:
            fails += 1
            if fails >= 3:
                on_revoked(); return
        time.sleep(60)

if __name__ == "__main__":
    session, plan, exp = activate(input("Clé: ").strip())
    print(f"Bienvenue. Plan={plan} expire={exp}")
    threading.Thread(target=heartbeat_loop,
                     args=(session, lambda: (print("Session révoquée."), sys.exit(0))),
                     daemon=True).start()
    # ... ton app ici ...
```

### C# (.NET 6+)

```csharp
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

public sealed class SakuAuth : IDisposable
{
    static readonly HttpClient http = new() { Timeout = TimeSpan.FromSeconds(10) };
    readonly string server, appId;
    readonly byte[] secret;
    CancellationTokenSource? beat;

    public SakuAuth(string server, string appId, string secretHex) {
        this.server = server; this.appId = appId;
        secret = Convert.FromHexString(secretHex);
    }

    string Hwid() {
        var seed = Environment.MachineName + "|" +
                   Environment.OSVersion.VersionString;
        return Convert.ToHexString(
            SHA256.HashData(Encoding.UTF8.GetBytes(seed))).ToLowerInvariant();
    }

    bool Verify(Dictionary<string, JsonElement> resp) {
        if (!resp.TryGetValue("signature", out var sigEl)) return false;
        var sig = sigEl.GetString() ?? "";
        resp.Remove("signature");
        var sorted = resp.OrderBy(k => k.Key).ToDictionary(k => k.Key, k => k.Value);
        var canon = JsonSerializer.Serialize(sorted,
            new JsonSerializerOptions { WriteIndented = false });
        var mac = new HMACSHA256(secret).ComputeHash(Encoding.UTF8.GetBytes(canon));
        return CryptographicOperations.FixedTimeEquals(
            Convert.FromHexString(sig), mac);
    }

    public async Task<string> ActivateAsync(string key) {
        var resp = await http.PostAsJsonAsync(
            $"{server}/api/v1/{appId}/activate",
            new { key, hwid = Hwid() });
        var doc = await resp.Content.ReadFromJsonAsync<Dictionary<string, JsonElement>>()
                  ?? throw new("réponse vide");
        if (!Verify(new(doc))) throw new("signature invalide");
        if (!doc["success"].GetBoolean())
            throw new(doc["message"].GetString() ?? "activation refusée");
        return doc["session"].GetString()!;
    }

    public void StartHeartbeat(string session, Action onRevoked) {
        beat = new();
        _ = Task.Run(async () => {
            while (!beat.IsCancellationRequested) {
                try {
                    var r = await http.PostAsJsonAsync(
                        $"{server}/api/v1/{appId}/heartbeat", new { session });
                    var d = await r.Content
                        .ReadFromJsonAsync<Dictionary<string, JsonElement>>();
                    if (d is null || !Verify(new(d)) ||
                        !d["success"].GetBoolean()) { onRevoked(); return; }
                } catch { }
                await Task.Delay(60_000, beat.Token);
            }
        }, beat.Token);
    }

    public void Dispose() => beat?.Cancel();
}
```

### Node.js

```js
import crypto from "node:crypto";
import os from "node:os";

const SERVER = "<TON_SERVER>";
const APP_ID = "<TON_APP_ID>";
const SECRET = Buffer.from("<TON_APP_SECRET>", "hex");

const hwid = () => crypto.createHash("sha256")
  .update(os.hostname() + "|" + os.arch() + "|" + os.cpus()[0].model)
  .digest("hex");

const verify = (resp) => {
  const { signature, ...rest } = resp;
  const canon = JSON.stringify(Object.fromEntries(
    Object.keys(rest).sort().map(k => [k, rest[k]])));
  const mac = crypto.createHmac("sha256", SECRET).update(canon).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(signature, "hex"),
                                Buffer.from(mac, "hex"));
};

export async function activate(key) {
  const r = await fetch(`${SERVER}/api/v1/${APP_ID}/activate`, {
    method: "POST", headers: {"Content-Type": "application/json"},
    body: JSON.stringify({ key, hwid: hwid() }),
    signal: AbortSignal.timeout(10_000),
  }).then(r => r.json());
  if (!verify({...r})) throw new Error("signature invalide");
  if (!r.success) throw new Error(r.message);
  return r.session;
}

export function startHeartbeat(session, onRevoked) {
  const tick = async () => {
    try {
      const r = await fetch(`${SERVER}/api/v1/${APP_ID}/heartbeat`, {
        method: "POST", headers: {"Content-Type": "application/json"},
        body: JSON.stringify({ session }),
        signal: AbortSignal.timeout(5_000),
      }).then(r => r.json());
      if (!verify({...r}) || !r.success) return onRevoked();
    } catch {}
    setTimeout(tick, 60_000);
  };
  tick();
}
```

---

## Où récupérer app_id + secret

Panel admin → onglet **Applications**. Chaque ligne a :

- son `app_id` (colonne APP ID, ex. `default`)
- son secret HMAC 32 hex (colonne SECRET, bouton ⧉ pour copier en entier)

Le secret est **spécifique à chaque app** — si tu crées deux produits
distincts (ex. "Aim SDK" et "Config Loader"), donne à chacun sa propre
app pour pouvoir les révoquer indépendamment.

## Vérifier que tout marche

```bash
curl -X POST https://sakumethod.fun/api/v1/<TON_APP_ID>/activate \
  -H "Content-Type: application/json" \
  -d '{"key":"SAKU-XXXX-XXXX-XXXX-XXXX","hwid":"testhwid"}'
```

Doit répondre soit `{"success":true,...}` soit `{"success":false,
"message":"Cle invalide"}` avec un champ `signature` en hex 64.
