# Pairing — connect a user's browser

Before OpenErrand can run an errand for one of your users, that user's browser must be
**paired** to `(your tenant, that user)` once. Pairing binds an identity — it does *not*
move any credentials. This page explains the one-click connect flow, what makes it safe,
and the fallbacks.

> **TL;DR.** You drop in OpenErrand's hosted connect button (one `<script>` tag) and
> provide one backend route that mints a pairing token. OpenErrand owns the rest — the
> popup, the handoff, and the Connect/Connected state. Your own site needs no
> browser-extension allowlisting — only `openerrand.app` talks to the extension. Your
> coding assistant can generate the token route + the script tag with the
> **`scaffold_web_pairing`** MCP tool.

## The flow

```
 Your app (app.acme.com)            OpenErrand                      The user
 ----------------------             ----------                      --------
 1. user clicks the embed button
 2. server: POST /pairing-tokens ──▶ relay mints a single-use,
    { userId }  (Bearer API key)     2-minute token bound to
                                      (tenant, userId)
 3. embed.js opens a popup window
    openerrand.app/connect.html ────▶ connect page:
    #token=…                          • POST /pairing-tokens/describe
                                        → verified app name/logo
                                      • sendMessage(extension, …) ──▶ extension parks it,
                                                                       prompts: "Allow Acme
                                                                       to run tasks in your
                                                                       browser?"
                                                                  ◀── 4. user clicks Approve
                                      relay redeems the token,
                                      binds (tenant, userId)
 5. extension closes the popup; the button now reads "OpenErrand Connected"
```

From then on, `client.run({ errandId, userId, … })` from your backend can target that
user's browser. The binding persists until the user disconnects the app in the side panel.

## Step 1 — mint a pairing token (server)

When the user is signed into your app, your **server** (authenticated with your API key)
mints a short-lived, single-use token bound to that user:

```ts
const r = await fetch(`${RELAY_HTTP}/pairing-tokens`, {
  method: "POST",
  headers: { authorization: `Bearer ${API_KEY}`, "content-type": "application/json" },
  body: JSON.stringify({ userId: "dana" }),
});
const { pairingToken } = await r.json();   // single-use, expires in ~2 minutes
```

The API key never leaves your server — only the minted token crosses to the browser.

## Step 2 — drop in the connect button (browser)

You write no connect JS — OpenErrand ships a hosted widget. Add one script tag (pointing
at the token route from Step 1) and a container:

```html
<script src="https://openerrand.app/embed.js"
        data-openerrand-token-url="/openerrand/pairing-token"
        data-openerrand-tenant="acme"></script>
<div data-openerrand-connect></div>
```

The widget renders the button, fetches a token from your route, opens a **small popup
window** (not a full-page navigation), runs the handoff, and shows **“Connect OpenErrand”**
vs **“OpenErrand Connected”** on its own — the connected state is read from the extension
via a hidden `openerrand.app` status frame, so there's no extra backend. That's the whole
client side.

## Step 3 — the connect page does the handoff

`openerrand.app/connect.html` is the **only** origin allowed to message the extension
(it's the lone entry in the extension's `externally_connectable` list). It:

1. calls `POST /pairing-tokens/describe` to resolve **whose** token this is, so the page —
   and the approval prompt — shows a name the relay vouches for, never a string supplied
   in the URL. The name is the **label of the API key** that minted the token (set in your
   dashboard, one key per site → "Allow **Acme Store** to run tasks?"), falling back to
   your tenant display name and then the tenant id. It's validated against the consent
   policy (no control characters, no impersonating the OpenErrand trust frame). This call
   is read-only and does **not** consume the token.
2. hands the token to the extension, which **parks** it and prompts the user
   **“Allow Acme to run tasks in your browser?”** — it never pairs silently.
3. on approval, the relay redeems the token and records the binding; the page returns the
   user to your `return` URL.

If the extension isn't installed, the connect page shows an install prompt instead. If the
token has expired or is malformed, it explains that and offers a way back.

## Why your site needs no allowlisting

A web page can only message an extension if the extension lists that page's origin in
`externally_connectable` — a list **frozen into the published extension** that only the
extension's author can change. Rather than ask every customer to get their origin added,
the handoff is centralized on `openerrand.app` (the embed widget opens the popup there),
so it works from any origin with zero extension configuration.

## What makes it safe — three independent gates

| Gate | Enforced by |
|---|---|
| The token is **single-use** and **relay-validated** (signed, ~2-minute expiry) | the relay, on redemption |
| The handoff comes from the **trusted `openerrand.app` origin** | the extension's `externally_connectable` allowlist |
| The user **explicitly approves** the named app | the extension's side-panel prompt — it never pairs silently |

Pairing establishes identity only. Destination-site **credentials never touch OpenErrand**
— they're reused from the user's existing session or filled from their on-device vault at
run time. See the [security model](./SECURITY_MODEL.md).

## Fallbacks

- **Manual paste.** If the widget can't run (e.g. the popup is blocked), the user can paste
  the pairing token into the side panel's **Connect an app** field. Same gates apply.
- **Enterprise auto-pair.** Managed deployments pair without a per-user click using a
  tenant-signed identity assertion — see [ENTERPRISE_DEPLOYMENT.md](./ENTERPRISE_DEPLOYMENT.md).

## Generate the code

The **`scaffold_web_pairing`** MCP tool emits the two pieces — the `/openerrand/pairing-token`
server route and the `<script>` tag for OpenErrand's hosted connect button — wired to your
errand. See the [integration guide](./INTEGRATION.md) for the end-to-end walkthrough.
