Docs/Pairing — connect a user's browser raw .md

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:

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:

<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.

#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.

#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 for the end-to-end walkthrough.