# SKELIA MEDIA HUB v3.0 — MASTER INTEGRATION BLUEPRINT (PACK AGENT IA & SDK B2B)

> **DOCUMENT OFFICIEL CONÇU POUR LES DÉVELOPPEURS ET LEURS AGENTS DE CODE IA (Cursor, Claude Code, GitHub Copilot, Windsurf, Codex, Devin).**
> Donnez ce fichier complet en contexte à votre agent IA avec l’instruction suivante :
> *"Implémente l’intégration complète du moteur vidéo Skelia Media Hub dans mon application en respectant scrupuleusement `SKELIA-PARTNER-AI-AGENT-MASTER-SPEC.md`."*

---

## 0. DIRECTIVES SYSTÈME IMPÉRATIVES POUR L’AGENT IA D’INTÉGRATION

Si vous êtes un agent IA chargé d’intégrer **Skelia Media Hub Partner API v1 / v3.0** dans une application tierce (SaaS e-commerce, boutique en ligne, application mobile, CRM vendeurs COD, ERP ou studio marketing), appliquez **strictement** ces 8 règles :

1. **Sécurité absolue des clés API (`skh_test_...`, `skh_live_...` et `SKELIA_HMAC_SECRET`)** :
   - Ne stockez et n’appelez **JAMAIS** la clé API ou le secret HMAC depuis le navigateur/mobile (`NEXT_PUBLIC_*`, `VITE_*`, `REACT_APP_*` strictement interdits).
   - Tous les appels vers `/api/partner/v1/*` s’exécutent **côté serveur** (Server-to-Server).
2. **Signature HMAC-SHA256 obligatoire en Production (`skh_live_...`)** :
   - En mode `skh_test_...` (Sandbox gratuit), seul l’en-tête `X-Skelia-Partner-Key` est requis.
   - En mode `skh_live_...` (Production), ajoutez systématiquement `X-Skelia-Timestamp` (epoch secondes) et `X-Skelia-Signature: sha256=<hex>` calculé sur `"${timestamp}.${rawBody}"` avec votre `hmac_signing_secret`.
3. **Identifiant sous-utilisateur déterministe (`skelia_user_id` au format UUID v4/v5)** :
   - `skelia_user_id` doit être un UUID (`xxxxxxxx-xxxx-4xxx-axxx-xxxxxxxxxxxx`) unique par client final sur votre plateforme. Si votre base de données utilise déjà des UUIDs (`user.id`), passez-les directement. Si vos IDs utilisateurs sont des entiers ou slugs (`"shop_42"`), convertissez-les de manière déterministe en UUID via la fonction `toDeterministicUuid(externalId)` fournie dans le SDK ci-dessous.
4. **Architecture financière « Crédits en Gros (Wholesale) »** :
   - **Skelia ne prélève jamais l’argent des clients finaux du partenaire.**
   - Le partenaire encaisse ses clients sur sa propre passerelle de paiement (Mobile Money, Wave, Orange Money, MTN, Stripe, Paystack, CinetPay, AlkaPay, abonnement interne) à son **propre tarif de revente** (ex. `250`, `300` ou `400 FCFA / crédit`).
   - Dès confirmation du paiement client sur le backend partenaire, appelez `POST /api/partner/v1/credits/grant` avec `transaction_id` unique, `skelia_user_id`, `email` et `montant` (entier de crédits). Skelia débite alors atomiquement votre **Stock de Crédits en Gros** et crédite le portefeuille isolé du client.
5. **Gestion du code HTTP `402 WHOLESALE_POOL_INSUFFICIENT`** :
   - Si votre stock de crédits en gros Skelia est insuffisant lors d’un `POST /api/partner/v1/credits/grant`, l’API répond `402` avec `error.code === "WHOLESALE_POOL_INSUFFICIENT"`. Enregistrez le paiement client en attente côté partenaire, alertez votre équipe, puis rejouez exactement le même `transaction_id` après recharge de votre stock en gros (zéro perte, zéro double crédit).
6. **Zéro bande passante vidéo sur le serveur partenaire** :
   - Appelez `POST /api/partner/v1/uploads/init` depuis votre backend, renvoyez `upload_url` + `media_ref` (`r2://uploads/<workspace_id>/...`) au navigateur de votre client, puis faites un `PUT` binaire direct vers Cloudflare R2.
7. **Rendu asynchrone + Liens MP4 opaques 24h compatibles HTTP 206 Range** :
   - En Sandbox (`skh_test_...`), les jobs se terminent **instantanément** (`status: "termine"`) avec de vrais médias de démonstration et **0 crédit consommé**.
   - En Production (`skh_live_...`), le rendu cloud (IA + Voix Neurale HD + Badges + FFmpeg) prend **45 à 110 secondes**. Utilisez `webhook_url` + un polling léger sur `GET /api/partner/v1/jobs/{job_id}` toutes les 5 secondes.
8. **Validation Smoke-Test obligatoire avant livraison** :
   - Exécutez toujours la suite de vérification de la Section 6 avec `skh_test_...` avant de conclure l’intégration.

---

## 1. FLUX D’ARCHITECTURE B2B « CRÉDITS EN GROS »

```text
[Client Final du Partenaire]
     │
     ├── 1. Achète ex: 24 crédits sur l'application du Partenaire (ex: 24 × 300 FCFA = 7 200 FCFA)
     ▼
[Serveur Backend du Partenaire]
     │
     ├── 2. Webhook paiement confirmé -> POST /api/partner/v1/credits/grant
     │      (Skelia débite -24 cr du Stock en Gros acheté ex: 150 FCFA/cr = 3 600 FCFA
     │       et crédite +24 cr sur le portefeuille isolé du client -> +3 600 FCFA de marge nette partenaire !)
     │
     ├── 3. Client sélectionne ses vidéos/photos produit -> Backend appelle POST /api/partner/v1/uploads/init
     │      Navigateur upload directement en PUT vers R2 -> obtient `media_ref` ("r2://uploads/...")
     │
     ├── 4. Client lance son montage -> Backend appelle POST /api/partner/v1/jobs/auto-edits
     │      (Débite 8 crédits du portefeuille du sous-utilisateur ; remboursement auto en cas d'échec)
     │
     └── 5. Skelia génère un spot unique (8 archétypes créatifs + Cerveau IA collectif + Voix HD + Badges)
            -> Webhook signé + `video_stream_url` (HTTP 206) & `video_download_url` prêts !
```

---

## 2. ENVIRONNEMENTS, URL DE BASE & SIGNATURE CRYPTOGRAPHIQUE

### 2.1 URL de Base API & Portail Web Partenaire
- **API Base URL** : `https://skelia-media-hub-api.skelia-media-hub.workers.dev`
- **Portail Self-Service Partenaire** : `https://skelia-media-hub.pages.dev/partenaires`

### 2.2 En-têtes HTTP Requis sur `/api/partner/v1/*`
```http
X-Skelia-Partner-Key: skh_live_<48_hex_chars>
X-Skelia-Timestamp: 1793534400
X-Skelia-Signature: sha256=<hex_hmac_sha256>
Content-Type: application/json
```
*(Astuce : `Authorization: Bearer skh_...` est également accepté en équivalent de `X-Skelia-Partner-Key`.)*

### 2.3 Calcul Exact de `X-Skelia-Signature`
```text
payload_to_sign = "${unix_timestamp_seconds}.${raw_request_body}"
signature_hex   = HMAC_SHA256(SKELIA_HMAC_SECRET, payload_to_sign)
X-Skelia-Signature = "sha256=" + signature_hex
```
- Pour une requête `GET` sans corps, `raw_request_body` est `""` -> signez `"${unix_timestamp_seconds}."`.
- Tolérance d’horloge serveur : `±300 secondes`.

---

## 3. CATALOGUE DES VOIX NEURALES HD & ANGLES CRÉATIFS v3.0

### 3.1 Voix Neurales HD (`voice`) — Prosodie Orale Naturelle
| Valeur `voice` | Langue (`target_language`) | Ton & Personnalité | Recommandation |
|---|---|---|---|
| `fr-dynamique` | `FR` | Homme · Énergique, punchy & commercial | E-commerce COD, gadgets, promos flash, maison |
| `fr-WA` | `FR` | Homme · Chaleureux, rassurant & fluide | Généraliste, électronique, Afrique & Diaspora |
| `fr-FR` | `FR` | Femme · Naturelle, souriante & claire | Beauté, cuisine, mode, bien-être, famille |
| `fr-vivienne` | `FR` | Femme · Premium, éditoriale & élégante | Cosmétiques haut de gamme, parfums, bijoux |
| `fr-confiance` | `FR` | Posée & institutionnelle | Services, B2B, santé, immobilier |
| `en-US` / `en-NG` / `en-femme` | `EN` | Voix commerciales anglophones | Boutiques internationales |
| `ar-MA` / `ar-femme` | `AR` | Voix commerciales arabophones | Maghreb & Moyen-Orient |

### 3.2 Angles Commerciaux (`sales_angle`)
- `auto` *(sélection dynamique anti-répétition parmi 8 structures créatives)*
- `problem_solution` *(Douleur client -> Démonstration produit -> Offre)*
- `direct_demo` *(Preuve visuelle choc dès la 1re seconde)*
- `cod_offer` *(Offre spéciale livraison rapide & paiement à la réception)*
- `ugc_review` *(Témoignage authentique façon retour client)*
- `three_proofs` *(3 arguments concrets rythmés)*
- `before_after` *(Transformation Avant / Après)*
- `premium_desire` *(Éditorial haut de gamme)*

---

## 4. RÉFÉRENCE EXACTE DES ENDPOINTS (`/api/partner/v1/*`)

### 4.1 Consulter votre Stock de Crédits en Gros & Tarifs
`GET /api/partner/v1/account`

Retourne votre solde actuel `wholesale_credit_balance`, votre tarif d’achat négocié `wholesale_unit_price_fcfa`, votre prix de revente configuré `retail_unit_price_fcfa` et le coût moteur des actions (`translation: 5`, `auto_editor: 8`).

---

### 4.2 Provisionner ou Relier un Sous-Utilisateur (Idempotent)
`POST /api/partner/v1/users/resolve`

**Corps JSON :**
```json
{
  "skelia_user_id": "550e8400-e29b-41d4-a716-446655440000",
  "email": "vendeur@votre-plateforme.com",
  "full_name": "Boutique Kora"
}
```

---

### 4.3 Attribuer des Crédits à un Client après Paiement (Débit du Stock en Gros)
`POST /api/partner/v1/credits/grant`

- Si `email` est fourni, Skelia provisionne automatiquement le sous-utilisateur s’il n’existe pas encore !
- En mode `skh_live_`, débite `montant` crédits de votre `wholesale_credit_balance` et crédite `+montant` sur le portefeuille du client.
- Supporte tout montant entier `1..10000` pour les clés d’organisation partenaire.

**Corps JSON :**
```json
{
  "transaction_id": "pay_wave_20261101_99012",
  "skelia_user_id": "550e8400-e29b-41d4-a716-446655440000",
  "email": "vendeur@votre-plateforme.com",
  "full_name": "Boutique Kora",
  "montant": 24,
  "reason": "Achat 24 crédits sur notre plateforme"
}
```

**Réponse `200 OK` :**
```json
{
  "ok": true,
  "environment": "live",
  "idempotent_replay": false,
  "grant": {
    "transaction_id": "pay_wave_20261101_99012",
    "skelia_user_id": "550e8400-e29b-41d4-a716-446655440000",
    "credits_added": 24,
    "user_new_balance": 24,
    "wholesale_pool_remaining": 1976
  }
}
```

---

### 4.4 Consulter le Solde & l’Historique d’un Sous-Utilisateur
- `GET /api/partner/v1/credits/balance?skelia_user_id=550e8400-e29b-41d4-a716-446655440000`
- `GET /api/partner/v1/credits/history?skelia_user_id=550e8400-e29b-41d4-a716-446655440000`

---

### 4.5 Initialiser un Upload Direct Navigateur -> Cloudflare R2
`POST /api/partner/v1/uploads/init`

**Corps JSON :**
```json
{
  "skelia_user_id": "550e8400-e29b-41d4-a716-446655440000",
  "kind": "video",
  "filename": "rush-produit-1.mp4",
  "content_type": "video/mp4",
  "size_bytes": 16400200
}
```

**Réponse `201 Created` (pour fichiers `<= 96 Mo`, mode `single_put`) :**
```json
{
  "ok": true,
  "mode": "single_put",
  "media_ref": "r2://uploads/<workspace_id>/videos/1793534400-a1b2-rush-produit-1.mp4",
  "upload_url": "https://...r2.cloudflarestorage.com/...",
  "method": "PUT",
  "required_headers": { "Content-Type": "video/mp4" },
  "expires_in_seconds": 3600
}
```
*(Pour les vidéos très lourdes `> 96 Mo` jusqu’à `4 Go`, l’endpoint renvoie `mode: "multipart"` avec `parts[].upload_url`, puis appelez `POST /api/partner/v1/uploads/complete`.)*

---

### 4.6 Lancer un Montage Publicitaire IA (`8 crédits`)
`POST /api/partner/v1/jobs/auto-edits`

**Corps JSON :**
```json
{
  "skelia_user_id": "550e8400-e29b-41d4-a716-446655440000",
  "title": "Mixeur Portable Sans Fil ProBlend",
  "brief": "Prépare un smoothie frais en 30 secondes partout. Rechargeable USB-C, 6 lames inox, étanche et facile à rincer.",
  "price_tag": "12 500 FCFA",
  "cta": "Commandez maintenant — Paiement à la livraison",
  "whatsapp_contact": "+225 07 00 00 00",
  "target_language": "FR",
  "voice": "fr-dynamique",
  "sales_angle": "auto",
  "trust_badge": "cod",
  "market_zone": "fcfa",
  "music_style": "dynamique",
  "target_duration": 35,
  "definition": "hd",
  "media_items": [
    { "url": "r2://uploads/<workspace_id>/videos/clip1.mp4", "type": "video" },
    { "url": "r2://uploads/<workspace_id>/videos/clip2.mp4", "type": "video" }
  ],
  "webhook_url": "https://api.votre-plateforme.com/webhooks/skelia"
}
```

---

### 4.7 Lancer une Traduction & Doublage Vidéo (`5 crédits`)
`POST /api/partner/v1/jobs/translations`

**Corps JSON :**
```json
{
  "skelia_user_id": "550e8400-e29b-41d4-a716-446655440000",
  "video_url": "r2://uploads/<workspace_id>/videos/source.mp4",
  "target_language": "FR",
  "voice": "fr-vivienne",
  "register": "commercial",
  "webhook_url": "https://api.votre-plateforme.com/webhooks/skelia"
}
```

---

### 4.8 Suivre un Job & Lister la Bibliothèque Vidéo d’un Client
- **Statut d’un job** : `GET /api/partner/v1/jobs/{job_id}?skelia_user_id=550e8400-e29b-41d4-a716-446655440000`
- **Bibliothèque complète du client** : `GET /api/partner/v1/library?skelia_user_id=550e8400-e29b-41d4-a716-446655440000`

Lorsque le rendu est prêt, `status === "termine"` et `result` contient :
- `result.video_stream_url` *(streaming direct `<video>` avec HTTP 206 Range, valide 24h et régénéré à chaque lecture)*
- `result.video_download_url` *(téléchargement direct fichier `.mp4`)*
- `result.voice_mp3_url` *(piste voix-off `.mp3`)*
- `result.campaign_kit` *(accroche, texte pub prêt à poster, script WhatsApp closing)*

---

## 5. SDK SERVEUR COMPLET PRÊT À COPIER-COLLER (TYPESCRIPT / NEXT.JS / NODE 20+)

Créez `src/lib/skelia-partner.ts` :

```typescript
import crypto from "node:crypto";

const SKELIA_BASE_URL = (
  process.env.SKELIA_API_BASE_URL ||
  "https://skelia-media-hub-api.skelia-media-hub.workers.dev"
).replace(/\/$/, "");
const SKELIA_PARTNER_KEY = process.env.SKELIA_PARTNER_API_KEY!;
const SKELIA_HMAC_SECRET = process.env.SKELIA_PARTNER_HMAC_SECRET || "";

const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;

/**
 * Convertit n'importe quel ID utilisateur interne (ex: "42", "shop_abc") en UUID déterministe stable.
 * Si l'ID est déjà un UUID valide, il est retourné tel quel.
 */
export function toSkeliaUserUuid(internalUserId: string): string {
  const trimmed = String(internalUserId || "").trim().toLowerCase();
  if (UUID_REGEX.test(trimmed)) return trimmed;
  const h = crypto.createHash("sha256").update(`skelia-partner-uid:${trimmed}`).digest("hex");
  return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-a${h.slice(17, 20)}-${h.slice(20, 32)}`;
}

export class SkeliaPartnerClient {
  private async request<T>(
    method: "GET" | "POST",
    path: string,
    body?: Record<string, unknown>,
    idempotencyKey?: string
  ): Promise<T> {
    if (!SKELIA_PARTNER_KEY) {
      throw new Error("Variable serveur SKELIA_PARTNER_API_KEY manquante.");
    }
    const rawBody = body ? JSON.stringify(body) : "";
    const ts = Math.floor(Date.now() / 1000).toString();

    const headers: Record<string, string> = {
      "X-Skelia-Partner-Key": SKELIA_PARTNER_KEY,
      Accept: "application/json",
    };
    if (body) headers["Content-Type"] = "application/json";
    if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;

    if (SKELIA_HMAC_SECRET) {
      const sig = crypto
        .createHmac("sha256", SKELIA_HMAC_SECRET)
        .update(`${ts}.${rawBody}`)
        .digest("hex");
      headers["X-Skelia-Timestamp"] = ts;
      headers["X-Skelia-Signature"] = `sha256=${sig}`;
    }

    const res = await fetch(`${SKELIA_BASE_URL}${path}`, {
      method,
      headers,
      body: body ? rawBody : undefined,
      cache: "no-store",
    });

    const json = await res.json().catch(() => ({}));
    if (!res.ok) {
      const msg = json?.error?.message || json?.error || `Skelia HTTP ${res.status}`;
      const err = new Error(msg) as Error & { status?: number; code?: string };
      err.status = res.status;
      err.code = json?.error?.code || json?.code;
      throw err;
    }
    return json as T;
  }

  /** 1. État du stock de crédits en gros du partenaire */
  getAccount() {
    return this.request("GET", "/api/partner/v1/account");
  }

  /** 2. Allouer des crédits au client dès confirmation d'un paiement sur votre site */
  grantCreditsAfterCustomerPayment(params: {
    internalUserId: string;
    email: string;
    fullName?: string;
    credits: number;
    paymentTxId: string;
  }) {
    const skelia_user_id = toSkeliaUserUuid(params.internalUserId);
    const tx = `pay_${params.paymentTxId}`;
    return this.request(
      "POST",
      "/api/partner/v1/credits/grant",
      {
        transaction_id: tx,
        skelia_user_id,
        email: params.email,
        full_name: params.fullName || "",
        montant: Math.trunc(params.credits),
        reason: `Customer payment ${params.paymentTxId}`,
      },
      tx
    );
  }

  /** 3. Solde de crédits d'un client */
  getUserBalance(internalUserId: string) {
    const uid = toSkeliaUserUuid(internalUserId);
    return this.request("GET", `/api/partner/v1/credits/balance?skelia_user_id=${encodeURIComponent(uid)}`);
  }

  /** 4. Générer une URL d'upload direct R2 pour le navigateur */
  initDirectUpload(params: {
    internalUserId: string;
    kind?: "video" | "image" | "logo";
    filename: string;
    contentType: string;
    sizeBytes: number;
  }) {
    return this.request<{
      ok: boolean;
      mode: string;
      media_ref: string;
      upload_url: string;
      required_headers: Record<string, string>;
    }>("POST", "/api/partner/v1/uploads/init", {
      skelia_user_id: toSkeliaUserUuid(params.internalUserId),
      kind: params.kind || "video",
      filename: params.filename,
      content_type: params.contentType,
      size_bytes: params.sizeBytes,
    });
  }

  /** 5. Lancer un Montage Publicitaire Auto-Editor v3.0 (8 crédits) */
  launchAutoEdit(params: {
    internalUserId: string;
    title: string;
    brief: string;
    priceTag?: string;
    cta?: string;
    whatsappContact?: string;
    language?: "FR" | "EN" | "AR";
    voice?: string;
    salesAngle?: string;
    mediaRefs: string[];
    webhookUrl?: string;
    idempotencyKey?: string;
  }) {
    const uid = toSkeliaUserUuid(params.internalUserId);
    return this.request(
      "POST",
      "/api/partner/v1/jobs/auto-edits",
      {
        skelia_user_id: uid,
        title: params.title,
        brief: params.brief,
        price_tag: params.priceTag || "",
        cta: params.cta || "Commandez dès maintenant, paiement à la livraison !",
        whatsapp_contact: params.whatsappContact || "",
        target_language: params.language || "FR",
        voice: params.voice || "fr-dynamique",
        sales_angle: params.salesAngle || "auto",
        media_items: params.mediaRefs.map((url) => ({ url })),
        webhook_url: params.webhookUrl,
      },
      params.idempotencyKey || `ae_${uid}_${Date.now()}`
    );
  }

  /** 6. Récupérer l'état d'un job ou la bibliothèque complète */
  getJob(jobId: string, internalUserId: string) {
    const uid = toSkeliaUserUuid(internalUserId);
    return this.request("GET", `/api/partner/v1/jobs/${encodeURIComponent(jobId)}?skelia_user_id=${encodeURIComponent(uid)}`);
  }

  listUserLibrary(internalUserId: string) {
    const uid = toSkeliaUserUuid(internalUserId);
    return this.request("GET", `/api/partner/v1/library?skelia_user_id=${encodeURIComponent(uid)}`);
  }
}

export const skeliaPartner = new SkeliaPartnerClient();
```

---

## 6. PROTOCOLE DE TEST AUTOMATISÉ POUR L’AGENT IA (SANDBOX `skh_test_`)

Avant de terminer votre tâche, exécutez ce scénario avec `SKELIA_PARTNER_API_KEY=skh_test_...` (coût = **0 crédit**, rendu instantané) :

1. `GET /api/partner/v1/account` -> doit retourner `200 OK` avec `"environment": "test"`.
2. `POST /api/partner/v1/credits/grant` avec `transaction_id: "test_tx_001"`, `skelia_user_id: "550e8400-e29b-41d4-a716-446655440000"`, `email: "demo@marchand.test"`, `montant: 24` -> doit retourner `200 OK`, puis rejouer le même appel -> vérifier `"idempotent_replay": true`.
3. `POST /api/partner/v1/jobs/auto-edits` avec `voice: "fr-dynamique"`, `sales_angle: "auto"`, `media_items: ["https://skelia-media-hub.pages.dev/sample.mp4"]` -> doit retourner immédiatement un job `"status": "termine"` avec `result.video_stream_url` et `result.video_download_url`.
4. Vérifier par recherche globale qu’aucune chaîne `skh_live_` ou `skh_test_` n’est exposée dans le code frontend.

---

*Skelia Media Hub v3.0 — Infrastructure Cloud de Montage Publicitaire & Doublage IA B2B.*
