# Desabenab embed picker and message API v2

`/embed` is a compact AC signal picker for an iframe. It uses the same catalogue, `/api/v2/capabilities/{profileId}` and `/api/v2/generate` endpoints as the main website. The host receives the **exact** successful generation response, including its actual carrier duty and CRC32. The picker does not transmit IR or observe the appliance.

## Embed URL

`https://YOUR_DESABENAB_ORIGIN/embed?parentOrigin=https%3A%2F%2FYOUR_HOST_ORIGIN&requestId=picker-1`

- `parentOrigin` is required and must be an exact HTTPS origin (scheme, host and optional port, with no path, trailing slash, credentials, query or fragment). For local previews only, `http://localhost` and `http://127.0.0.1` with an optional port are accepted. Wildcards, `null`, `file:` and other HTTP origins are rejected.
- `requestId` is required: 1–128 ASCII letters, digits, hyphens or underscores. Set a distinct ID for every iframe instance. It is echoed in every message.
- The picker sends messages **only** to `window.parent` using the validated `parentOrigin` as the exact `postMessage` target origin. Missing or invalid options disable the picker and produce a visible setup error; no message is sent when the parent origin cannot be validated.
- If opened directly for preview, the picker works with valid options but has no parent window to message.

## Messages

All messages are structured-clone objects with `version: 2`, `requestId`, `selectionRevision` (a nonnegative integer that increases on every selection change), and a `type` below. Receivers should ignore unknown versions and types. The picker does not accept incoming commands.

| `type` | Additional fields | Meaning |
| --- | --- | --- |
| `desabenab:ready` | `state: "ready" \| "selection-changed"` | Initial controls are ready, or a selection changed. Clear any result on every ready message. |
| `desabenab:result` | `device: {brand, model, profileId}`, `result` | `result` is the unmodified JSON object returned by `POST /api/v2/generate`. It includes `settings`, `signal`, `frequencyHz`, actual `carrierDutyPercent`, and unsigned `crc32`. `signal` is `[frequencyHz, markUs, spaceUs, ..., crc32]`. |
| `desabenab:error` | `error: {code, message}` | Capability loading or generation failed. No result is delivered. Clear any prior result. |

The `selectionRevision` on a result belongs to the controls used for that API request. If a user changes a selection while generation is pending, the picker discards the old response. `ready` with `state: "selection-changed"` tells the host to clear its previous result. For `power: "off"`, `settings` contains only `{power:"off"}`. For toggle-only remotes, `settings.power` is `"toggle"` and does not state the appliance's current power state.

## Copyable host integration

Replace the picker origin below with your deployed Desabenab origin. Serve the host page over HTTPS. The `message` listener checks the sender origin, iframe window, type, version and request ID before using the result. A runnable version is in [`examples/embed-host.html`](examples/embed-host.html); serve it on a **different origin** from the picker to test cross-origin behavior.

```html
<iframe id="ac-picker" title="Choose an AC infrared signal" style="width:100%;height:680px;border:0"></iframe>
<pre id="ac-result" aria-live="polite">Waiting for picker…</pre>
<script>
  const pickerOrigin = 'https://YOUR_DESABENAB_ORIGIN';
  const frame = document.getElementById('ac-picker');
  const output = document.getElementById('ac-result');
  const requestId = crypto.randomUUID();
  const url = new URL('/embed', pickerOrigin);
  url.searchParams.set('parentOrigin', window.location.origin);
  url.searchParams.set('requestId', requestId);
  frame.src = url.href;

  window.addEventListener('message', event => {
    if (event.origin !== pickerOrigin || event.source !== frame.contentWindow) return;
    const message = event.data;
    if (!message || typeof message !== 'object' || message.version !== 2 ||
        message.requestId !== requestId ||
        !['desabenab:ready', 'desabenab:result', 'desabenab:error'].includes(message.type)) return;
    if (message.type === 'desabenab:ready') output.textContent = 'Choose settings and generate a signal.';
    if (message.type === 'desabenab:error') output.textContent = message.error.message;
    if (message.type === 'desabenab:result') {
      const {device, result} = message;
      output.textContent = JSON.stringify({device, settings: result.settings,
        signal: result.signal, frequencyHz: result.frequencyHz,
        carrierDutyPercent: result.carrierDutyPercent, crc32: result.crc32}, null, 2);
    }
  });
</script>
```

Do not use `targetOrigin: "*"`, trust `event.data` without checking its source, or send an appliance command solely because a picker message arrived. The generated array is data for the host's own IR transport.

## Framing and deployment

The main website (`/` and `/index.html`) keeps `X-Frame-Options: DENY` and CSP `frame-ancestors 'none'`. Only `/embed` and `/embed.html` use CSP `frame-ancestors *` and omit `X-Frame-Options`; this allows arbitrary third-party hosts to embed the picker. The explicit `parentOrigin` controls message delivery. The native server applies these headers directly. `node build.mjs` writes equivalent Cloudflare Pages rules to `dist/_headers`; Pages' route-specific rules are required because its header rules otherwise accumulate. Do not apply a blanket `X-Frame-Options: DENY` or `frame-ancestors 'none'` at a reverse proxy/CDN to `/embed`, and do not remove the main page's policy. Check both routes' response headers after deployment.

On Cloudflare Free, the picker is static on Pages and the on-demand API is a separate Worker. Set `DESABENAB_API_BASE` to the Worker's HTTPS origin during the Pages build. The generated CSP permits that API origin in `connect-src`; the API allows credential-free CORS. The Pages origin returns 404 for `/api/*`, so use the Worker origin when calling the API directly. See [CLOUDFLARE.md](CLOUDFLARE.md) for deployment and rate limits. The static build also publishes `/embed-host`, a runnable host example; use a **different origin** for its `pickerOrigin` query parameter when checking cross-origin behavior. For a local preview, run `node server.mjs`, then run `python3 -m http.server 8789 --bind 127.0.0.1` from `examples/` and open `http://localhost:8789/embed-host.html`. The picker URL inside that host uses `parentOrigin=http://localhost:8789` and a unique request ID.
