TR

Intégrer une application JumPHP sur le site d'un partenaire

Le kit et l'extension WordPress sont dans le dépôt JumPHP : https://git.jumphp.net/jumphp/jumPhp/src/branch/main/tools/embed

Une application publiée sur le playground (https://jumphp.net) peut apparaître sur un autre site comme une de ses pages : pas d'iframe, pas de CORS, même origine. Les moteurs de recherche voient le HTML sur le site du partenaire (balise canonical vers son adresse), et l'application reçoit la provenance du visiteur (le site d'où il vient) et les champs du formulaire qui l'a envoyée.

Exemple réel : caramail-chat sur https://caramail.tv : en tête de chaque page un formulaire de connexion (un bloc servi par l'application, mis à jour avec elle) qui envoie vers https://caramail.tv/chat/, où l'application tourne comme si elle y était hébergée.

navigateur ──► caramail.tv/chat/...  ──(requête signée)──►  jumphp.net/api/embed/caramail-chat/app/...
               serveur du partenaire  ◄──── réponse ─────   (page réécrite, moteur, fichiers)
               (kit jumphp-embed.php)
navigateur ──► wss://jumphp.net/tcp (pont TCP, direct : un WebSocket n'a pas de CORS)

1. Côté application (le projet)

config/project.yaml :

project:
  embed:
    blocks: [login]                  # site/blocks/login.html : des fragments HTML que le partenaire affiche
    params: [nickname, age, city, sex]   # les champs de formulaire transmis à l'application (scène de départ)
  • Un bloc est un fragment HTML (site/blocks/<nom>.html), sans <html> : un formulaire, une bannière, une liste de salons… {{BASE}} y devient l'adresse de l'application chez le partenaire (https://caramail.tv/chat), {{SITE}} son domaine, {{APP}} le slug. Ses images : {{BASE}}/site/images/logo.png (les fichiers de site/, relayés aussi).
    <form class="jumphp-login" method="get" action="{{BASE}}/">
      <input name="nickname" required maxlength="20" placeholder="Pseudo">
      <button>Entrer dans le chat</button>
    </form>
    
  • Les paramètres arrivent dans enter() / loaded() de la scène de départ, par-dessus start_params de project.yaml : seuls les noms déclarés dans embed.params, en chaînes, et toujours site = le domaine du partenaire (mis par le playground d'après la signature : jamais celui que le visiteur écrirait dans l'adresse).
    protected function loaded(array $params = []): void
    {
        $this->nickname->text = (string)($params['nickname'] ?? '');
        $this->from = (string)($params['site'] ?? 'jumphp.net');      // caramail.tv
    }
    
  • Le formulaire peut être method="get" (les champs sont dans l'adresse) ou method="post" (le kit les relaie tels quels).
  • Une application en terminal (kind: cli) ne s'intègre pas encore. Les jeux, les applications graphiques (kind: gui) et les applications de bureau dans le navigateur (ui: webview) oui.

Comment le moteur reçoit les paramètres : la page définit window.JUMPHP_GAME.start_params (→ la constante JUMPHP_START_PARAMS, fusionnée par Engine::startParams() sur start_params) et tcp_ticket_url (→ JUMPHP_TCP_URL) dans les moteurs publiés après JumPHP 0.5.53 ; pour toutes les versions du moteur, le playground sert aussi à cette page un config/project.yaml qui contient déjà ces start_params et ce tcp_ticket_url (les paramètres voyagent scellés, HMAC, de la page au manifeste).

2. Côté playground (le propriétaire du projet)

Studio → le projet → Sur le site d'un partenaire : domaine (caramail.tv), chemin de base (/chat) → Ajouter. Le studio donne :

  • l'identifiant du site (JUMPHP_EMBED_SITE=es_…, public) ;
  • le secret partagé (JUMPHP_EMBED_SECRET=jes_…, gardé chiffré, affichable par le propriétaire seulement).

Il peut désactiver / réactiver le site, renouveler le secret (l'ancien cesse aussitôt) ou le supprimer. L'administrateur voit tous les sites partenaires (Administration → Sites partenaires et domaines) et peut les couper (avec une raison) : le propriétaire ne peut alors pas le rétablir.

3. Côté partenaire : le kit

tools/embed/jumphp-embed.php : un seul fichier PHP (7.4+, curl), sans dépendance. Il :

  • sert l'application sous le chemin de base : /chat/ = la page, et tout ce qu'elle charge (engine/<version>/… le moteur partagé et son wasm, manifest.json, files/…, private/… les fichiers privés, embed-config.yaml, site/…) ;
  • signe chaque requête, relaie en flux (les gros fichiers ne passent jamais en mémoire), avec Range, If-None-Match, et le gzip du playground transmis tel quel ;
  • garde sur disque les fichiers immuables (adresse avec ?v=<hash> ou dossier versionné du moteur, jusqu'à 64 Mo) et les sert ensuite lui-même (Range compris) ;
  • garde les blocs sur disque (ttl, 300 s par défaut), les revalide avec leur ETag, et rend la dernière copie si le playground ne répond pas ;
  • relaie le ticket du pont TCP (/chat/api/tcp/ticket) et le réveil du runtime (/chat/api/runtime/wake) avec l'adresse du visiteur (X-JumPHP-Client-IP) : les limites par adresse et le journal de l'administrateur sont ceux du visiteur ;
  • ne transmet jamais les cookies du visiteur au playground, ni les cookies ou en-têtes X-Robots-Tag du playground au visiteur ; ajoute X-Content-Type-Options, Referrer-Policy, X-Frame-Options: SAMEORIGIN.

Réglages (tableau ou variables d'environnement) :

Clé Variable
server JUMPHP_EMBED_SERVER https://jumphp.net
app JUMPHP_EMBED_APP le slug de l'application (caramail-chat)
site JUMPHP_EMBED_SITE l'identifiant es_…
secret JUMPHP_EMBED_SECRET le secret jes_… (jamais dans git ni dans un fichier public)
base JUMPHP_EMBED_BASE le chemin déclaré (/chat)
cache JUMPHP_EMBED_CACHE un dossier inscriptible (défaut : dossier temporaire)
ttl JUMPHP_EMBED_TTL secondes de fraîcheur d'un bloc (300)
require '/srv/jumphp-embed/jumphp-embed.php';
$jumphp = JumphpEmbed::fromConfig(['app' => 'caramail-chat', 'base' => '/chat']);   // site, secret : l'environnement
if ($jumphp->serve()) { exit; }          // /chat/... : répondu ; sinon le site continue
echo $jumphp->block('login');            // dans une page

Exemples prêts : tools/embed/examples/nginx.conf (PHP-FPM, sans tampon), tools/embed/examples/Caddyfile, tools/embed/examples/front.php (n'importe quel site PHP). Le plus rapide est que nginx / Caddy envoient /chat/ directement au kit, sans charger le CMS.

WordPress

tools/embed/wordpress/build.sh fabrique jumphp-embed.zip (Extensions → Ajouter → Téléverser). Réglages → JumPHP Embed : l'adresse du playground et une ligne par application (slug, identifiant, secret, chemin de base). Le secret peut rester hors de la base : define('JUMPHP_EMBED_SECRET_CARAMAIL_CHAT', 'jes_…'); dans wp-config.php.

  • Shortcode : [jumphp_block app="caramail-chat" block="login"] (dans une page, un widget, ou do_shortcode() dans l'en-tête du thème pour l'avoir sur toutes les pages).
  • Le chemin de base (/chat/…) est intercepté dès plugins_loaded, avant le thème et la requête WordPress : la page et ses fichiers sont relayés et rien d'autre de WordPress ne s'exécute. Un cache de page (WP Rocket, W3TC, Varnish…) doit exclure /chat/ (la page porte les paramètres du visiteur).

4. Le pont TCP (applications qui ouvrent des connexions)

Par défaut une page demande son ticket à sa propre origine (/api/tcp/ticket). Chez le partenaire, l'origine est caramail.tv : la page intégrée demande donc https://caramail.tv/chat/api/tcp/ticket (tcp_ticket_url), que le kit relaie, signé, vers le playground. Le ticket désigne le pont wss://jumphp.net/tcp, que le navigateur ouvre directement (un WebSocket n'est pas soumis au CORS) : c'est donc le pont de jumphp.net, avec les mêmes autorisations de l'administrateur (l'application doit être approuvée pour TCP, docs/tcp.md). Ce choix garde la règle « même origine » pour tout ce qui est HTTP et évite d'ouvrir le CORS de l'API des tickets à n'importe quel site.

5. L'API (pour écrire un autre kit)

Toutes les requêtes portent :

En-tête
X-JumPHP-Site l'identifiant es_…
X-JumPHP-Time l'heure Unix ; refusée à plus de 300 s de l'horloge du playground
X-JumPHP-Signature hex de HMAC-SHA256(secret, MÉTHODE\nchemin\nrequête\nheure\nsha256hex(corps))

chemin et requête tels qu'envoyés (encodés, la requête sans ?, vide s'il n'y en a pas) ; corps vide pour un GET. Comparaison à temps constant. Test : secret s3cret, GET /api/embed/a/block/login, heure 1000, sans requête ni corps → hash_hmac('sha256', "GET\n/api/embed/a/block/login\n\n1000\n" . hash('sha256', ''), 's3cret').

Requête Réponse
GET /api/embed/<slug>/block/<nom> le bloc ({{BASE}}, {{SITE}}, {{APP}} remplacés), ETag, Cache-Control: public, max-age=300, 304 sur If-None-Match
GET|POST /api/embed/<slug>/app/ la page de l'application réécrite : <base href="https://<domaine><chemin>/">, moteur et fichiers en adresses relatives, canonical et og:url vers le partenaire, pas de PWA du playground ; la requête (et le corps d'un POST) donne les paramètres
GET /api/embed/<slug>/app/<chemin> manifest.json, embed-config.yaml, files/…, private/…, packages/…, app/…, engine/<version>/… (immuable), site/…
POST /api/embed/<slug>/tcp/ticket le ticket du pont, comme /api/tcp/ticket (X-JumPHP-Client-IP : l'adresse du visiteur)
POST /api/embed/<slug>/runtime/wake le réveil du runtime Android, comme /api/runtime/wake

Erreurs en JSON {"error": "..."} : 401 (signature absente, fausse, trop vieille, site inconnu), 403 (désactivé par le propriétaire, coupé par l'administrateur), 404 (application non publiée, bloc non déclaré, fichier absent), 405.

Une requête rejouée dans la fenêtre de 5 minutes rend la même chose (lectures) ; le secret ne circule jamais. Renouvelez-le s'il a fui : l'ancien cesse à l'instant.