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 desite/, 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-dessusstart_paramsde project.yaml : seuls les noms déclarés dansembed.params, en chaînes, et toujourssite= 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) oumethod="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-Tagdu playground au visiteur ; ajouteX-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, oudo_shortcode()dans l'en-tête du thème pour l'avoir sur toutes les pages). - Le chemin de base (
/chat/…) est intercepté dèsplugins_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.