# Embedding a JumPHP application on a partner's site

> The kit and the WordPress plugin are in the JumPHP repository: https://git.jumphp.net/jumphp/jumPhp/src/branch/main/tools/embed

An application published on the playground (https://jumphp.net) can appear on another site **as one of its own pages**: no
iframe, no CORS, same origin. Search engines see the HTML on the partner's site (a `canonical` to its address), and the
application gets the visitor's **provenance** (the site they came from) and the fields of the form that sent them.

Real case: `caramail-chat` on https://caramail.tv: a login form at the top of every page (a *block* served by the application,
updated with it) that sends to https://caramail.tv/chat/, where the application runs as if it were hosted there.

```
browser ──► caramail.tv/chat/...  ──(signed request)──►  jumphp.net/api/embed/caramail-chat/app/...
            partner's server       ◄──── answer ───────   (rewritten page, engine, files)
            (jumphp-embed.php kit)
browser ──► wss://jumphp.net/tcp (TCP bridge, direct: a WebSocket has no CORS)
```

## 1. The application (the project)

`config/project.yaml`:

```yaml
project:
  embed:
    blocks: [login]                       # site/blocks/login.html: HTML fragments the partner shows
    params: [nickname, age, city, sex]    # the form fields passed to the application (first scene)
```

- A block is an HTML fragment (`site/blocks/<name>.html`), no `<html>`: a form, a banner, a list of rooms… `{{BASE}}` becomes the
  application's address on the partner's site (`https://caramail.tv/chat`), `{{SITE}}` its domain, `{{APP}}` the slug. Its
  pictures: `{{BASE}}/site/images/logo.png` (the files of `site/` are relayed too).
  ```html
  <form class="jumphp-login" method="get" action="{{BASE}}/">
    <input name="nickname" required maxlength="20" placeholder="Nickname">
    <button>Enter the chat</button>
  </form>
  ```
- The **params** reach `enter()` / `loaded()` of the first scene, over project.yaml's `start_params`: only the names declared in
  `embed.params`, as strings, and **always `site`** = the partner's domain (set by the playground from the signature: never what
  the visitor could type in the address).
- The form may be `method="get"` (fields in the address) or `method="post"` (the kit relays them as they are).
- A terminal application (`kind: cli`) cannot be embedded yet. Games, graphic apps (`kind: gui`) and desktop apps in the browser
  (`ui: webview`) can.

How the engine gets the params: the page sets `window.JUMPHP_GAME.start_params` (→ constant `JUMPHP_START_PARAMS`, merged by
`Engine::startParams()` over `start_params`) and `tcp_ticket_url` (→ `JUMPHP_TCP_URL`) in the engines released after JumPHP 0.5.53; for **every** engine
version, the playground also serves that page a `config/project.yaml` that already holds these `start_params` and this
`tcp_ticket_url` (the params travel sealed, HMAC, from the page to its manifest).

## 2. The playground (the project's owner)

Studio → the project → **On a partner's site**: domain (`caramail.tv`), base path (`/chat`) → **Add**. The studio gives:

- the site's **identifier** (`JUMPHP_EMBED_SITE=es_…`, public);
- the shared **secret** (`JUMPHP_EMBED_SECRET=jes_…`, kept encrypted, shown to the owner only).

The owner can disable / enable it, **renew the secret** (the old one stops at once) or delete it. The administrator sees every
partner site (Administration → *Partner sites and domains*) and can **cut** one (with a reason): the owner cannot restore it.

## 3. The partner: the kit

`tools/embed/jumphp-embed.php`: one PHP file (7.4+, curl), no dependency. It:

- serves the application under the base path: `/chat/` = the page, and everything it loads (`engine/<version>/…` the shared engine
  and its wasm, `manifest.json`, `files/…`, `private/…`, `embed-config.yaml`, `site/…`);
- signs each request and relays it **streamed** (big files never sit in memory), with `Range`, `If-None-Match`, and the
  playground's gzip passed through;
- keeps immutable files **on disk** (`?v=<hash>` addresses and the engine's versioned folder, up to 64 MB) and then serves them
  itself (Range included);
- keeps the **blocks** on disk (`ttl`, 300 s by default), revalidates them with their ETag, and returns the last copy when the
  playground does not answer;
- relays the **TCP bridge ticket** (`/chat/api/tcp/ticket`) and the runtime wake-up (`/chat/api/runtime/wake`) with the visitor's
  address (`X-JumPHP-Client-IP`);
- **never** forwards the visitor's cookies to the playground, nor the playground's cookies or `X-Robots-Tag` to the visitor; adds
  `X-Content-Type-Options`, `Referrer-Policy`, `X-Frame-Options: SAMEORIGIN`.

Settings (an array or environment variables): `server` / `JUMPHP_EMBED_SERVER` (`https://jumphp.net`), `app` / `JUMPHP_EMBED_APP`,
`site` / `JUMPHP_EMBED_SITE`, `secret` / `JUMPHP_EMBED_SECRET` (never in git nor in a public file), `base` / `JUMPHP_EMBED_BASE`,
`cache` / `JUMPHP_EMBED_CACHE`, `ttl` / `JUMPHP_EMBED_TTL`.

```php
require '/srv/jumphp-embed/jumphp-embed.php';
$jumphp = JumphpEmbed::fromConfig(['app' => 'caramail-chat', 'base' => '/chat']);   // site, secret: the environment
if ($jumphp->serve()) { exit; }          // /chat/...: answered; otherwise the site goes on
echo $jumphp->block('login');            // in a page
```

Ready examples: `tools/embed/examples/nginx.conf`, `tools/embed/examples/Caddyfile`, `tools/embed/examples/front.php`. Fastest:
nginx / Caddy send `/chat/` straight to the kit without loading the CMS. Its tests: `tools/embed/tests/test.sh` (Docker).

### WordPress

`tools/embed/wordpress/build.sh` makes `jumphp-embed.zip` (Plugins → Add → Upload). Settings → **JumPHP Embed**: the playground's
address and one line per application (slug, identifier, secret, base path). The secret can stay out of the database:
`define('JUMPHP_EMBED_SECRET_CARAMAIL_CHAT', 'jes_…');` in `wp-config.php`.

- Shortcode: `[jumphp_block app="caramail-chat" block="login"]` (a page, a widget, or `do_shortcode()` in the theme's header).
- The base path (`/chat/…`) is taken at `plugins_loaded`, before the theme and WordPress's query. A page cache must exclude `/chat/`.

## 4. The TCP bridge

A page asks its ticket from **its own origin** by default (`/api/tcp/ticket`). On the partner's site that origin is `caramail.tv`,
so the embedded page asks `https://caramail.tv/chat/api/tcp/ticket` (`tcp_ticket_url`), which the kit relays, signed. The ticket
names the bridge `wss://jumphp.net/tcp`, opened directly by the browser (a WebSocket is not subject to CORS), with the same
administrator approvals (docs/tcp.md). Every HTTP request stays same-origin, and the tickets API is not opened to every site's CORS.

## 5. The API (to write another kit)

Headers of every request: `X-JumPHP-Site` (`es_…`), `X-JumPHP-Time` (Unix time, refused beyond **300 s** from the playground's
clock), `X-JumPHP-Signature` = hex HMAC-SHA256(secret, `METHOD\npath\nquery\ntime\nsha256hex(body)`), path and query as sent
(encoded, query without `?`), compared in constant time. Test vector: secret `s3cret`, `GET /api/embed/a/block/login`, time `1000`,
no query nor body → `hash_hmac('sha256', "GET\n/api/embed/a/block/login\n\n1000\n" . hash('sha256', ''), 's3cret')`.

| Request | Answer |
|---|---|
| `GET /api/embed/<slug>/block/<name>` | the block, `ETag`, `Cache-Control: public, max-age=300`, 304 on `If-None-Match` |
| `GET\|POST /api/embed/<slug>/app/` | the rewritten page (`<base>`, relative engine and files, `canonical` / `og:url` on the partner); query and POST body give the params |
| `GET /api/embed/<slug>/app/<path>` | `manifest.json`, `embed-config.yaml`, `files/…`, `private/…`, `packages/…`, `app/…`, `engine/<version>/…`, `site/…` |
| `POST /api/embed/<slug>/tcp/ticket` | the bridge ticket (`X-JumPHP-Client-IP`: the visitor's address) |
| `POST /api/embed/<slug>/runtime/wake` | the runtime wake-up |

Errors as JSON `{"error": "..."}`: 401 (signature missing, wrong, too old, unknown site), 403 (off, or cut by the administrator),
404 (not published, undeclared block, missing file), 405.
