# Embed a Space — one line, and the drop-in doesn't cost you the encryption

> **Status: live.** The host-side SDK is served at `https://witbitz.chat/embed.js`; the iframe seam ships in the Spaces
> app. A working demo — a third-party page embedding a real Space with a live event log — is at
> [`witbitz.chat/site/embed-demo.html`](https://witbitz.chat/site/embed-demo.html). Cross-origin, browser-verified.

## The one-liner

```html
<script src="https://witbitz.chat/embed.js"></script>
<witbitz-space link="https://witbitz.chat/space#<your link>"></witbitz-space>
```

or drive it from script:

```js
const space = Witbitz.embed('#slot', { link })
space.on('message', m => …)   // someone posted (metadata only)
space.on('turn',    t => …)   // an agent turn finished
space.signIn(token)           // owner-gated rooms: admit the user at their tier
```

`Witbitz.embed(target, opts)` returns a controller: `on(type, cb)` / `off`, `signIn(token)`, `navigate(to)`,
`setTheme(theme)`, `destroy()`, and `iframe`. `<witbitz-space>` re-exposes it as `el.space`.

## Why it's an iframe — and why that's the point

That one line mounts a **cross-origin iframe on the Spaces origin**, not inline DOM in your page. That isn't a
limitation to apologize for — it's what keeps the guarantees true when a Space is embedded:

- The **verified** Space client runs in **its own origin**. Your page — and every tracker and third-party script on it
  — **cannot reach into it**, cannot read the room key, cannot read the plaintext.
- If a Space were inlined into your page, your page would become part of its trusted computing base, and *content-blind*
  would quietly stop being true the moment anyone embedded it. The iframe is the isolation boundary.

So a drop-in embed **does not cost you the encryption** — the part that touches your users' content still runs only in
verified code, in a context your page can't see.

## The bridge (host ⇄ Space)

The only channel across an origin boundary is `window.postMessage`. The SDK layers a small, **origin-checked** protocol
(JSON-RPC 2.0) on it. It is a **narrow capability seam, not shared memory** — the host can do exactly these things:

| Direction | Message | Meaning |
|---|---|---|
| Space → host | `ready` | the Space handshaked; the bridge is open |
| Space → host | `message` | someone posted — **metadata only** (`{id, self, sys}`), never content |
| Space → host | `turn` | an agent turn finished |
| Space → host | `member` | a member event |
| Space → host | `resize` | content height changed (auto-sizes the iframe) |
| host → Space | `signIn` | hand in an owner IdP token / signed grant |
| host → Space | `navigate` / `theme` | deep-link / theme within limits |

Discipline that makes the seam safe: **origin-checked both directions** (the Space only accepts control from the one
validated embedder; the host trusts only the Spaces origin), **nothing is posted to `'*'`**, and **the room key never
crosses the bridge** — it rides the iframe's URL fragment, client-side, and is never sent to a server. Sign-in is a
**verified token**, not a trust-me flag: it's checked against the room's sealed allow-list and resolved to a tier (see
[the owner rule](./the-owner-rule.md)).

One honesty note: the app that *embeds* a room necessarily holds that room's link, so it holds that room's key — the
isolation doesn't hide the embedded room from the embedder. What it protects is (a) the platform-wide guarantee that
*Witbitz* can't read content, and (b) the room from your page's *other* scripts and trackers.

## Who may embed — two gates

Embedding is **off by default** (`frame-ancestors 'self'`). Turning it on is two layers, deliberately:

- **`frame-ancestors` — the app-wide COARSE gate** (browser-enforced). It *has* to be coarse: the browser evaluates the
  header before any JavaScript runs, when the room isn't even known (especially for `#mk=` fragment links). Set the
  allowed embed origins for a deployment via `SPACE_EMBED_ORIGINS` (it feeds both this header and the client check from
  one source, so they can't drift).
- **The owner policy — the per-room PRECISE gate** (client-enforced). The owner's **signed** `AppPolicy` may name
  `embedOrigins` — the origins allowed to embed *that app's* rooms. Because the client holds the room key, it reads the
  sealed, signed policy itself and **closes the bridge** if this embedder isn't on the list. The server never sees it —
  content-blindness is preserved. On the hosted multi-tenant platform the owner key is resolved from the tenant registry
  (`op:'policy'`); on a pinned single-owner deployment it's the baked key.

So the browser stops any non-registered origin from framing at all; the owner policy narrows it to *this owner's* origins
per room. Fail-closed.

## Orthogonal to what the Space *is*

The embed is transport + isolation. Whether the client is *trustworthy code* is a separate axis —
[the attested client](./the-attested-client.md); admission and tiers are [the owner rule](./the-owner-rule.md). They
compose: an embedded Space is still content-blind, still owner-governed, still verifiable.

One line: **`witbitz.chat/embed.js` drops a real, isolated, content-blind Space into any page, talks to it over a narrow
origin-checked bridge that never carries your plaintext, and lets the room's owner decide who may embed it.**
