# Southbill App Bridge

The official communication layer between an embedded app and the Southbill Dashboard

The **Southbill App Bridge** is the official, supported way for an embedded app to talk to the Southbill Dashboard around it. Your app stays sandboxed inside its iframe; everything that belongs to the host — navigation, external pages, OAuth, toasts, modals, resizing, session tokens — is requested through the Bridge and executed by Southbill.

Use the Bridge instead of raw `postMessage`, `window.top.location`, `target="_blank"` links or framing external providers. Those either break in the sandbox or are blocked by the host.

## 1. Load the SDK

```html
<script src="https://www.southbill.com/southbill-app-bridge.js"></script>
```

```js
const bridge = SouthbillAppBridge.create();      // reads ?host= from the URL
const ctx = await bridge.ready();                // waits for southbill:ready

console.log(ctx.installation_id, ctx.environment);
```

`create()` accepts `{ host }` if you prefer to pass the host origin explicitly. It throws when no `host` query parameter is present — that means the page was not opened by Southbill.

The SDK is a thin wrapper: every call is a `postMessage` to the host origin, every answer is matched back by request id. Requests time out after 15 seconds and reject.

## 2. Lifecycle

| Step | What happens |
| --- | --- |
| 1 | Merchant clicks **Open** in *Dashboard → Apps* |
| 2 | Southbill loads `embedded_url?session_token=…&installation_id=…&environment=…&host=…` in a sandboxed iframe |
| 3 | The host posts `southbill:ready` with the app context |
| 4 | `bridge.ready()` resolves — your app may now use every Bridge function |
| 5 | Your app verifies the bootstrap `session_token` server-side and creates its own session |

Attach your listener (i.e. call `create()`) **before** any `await` so the `ready` event is never missed.

## 3. Function reference

### `bridge.ready(): Promise<Context>`
Resolves with the context once the host announced itself. Safe to call multiple times.

### `bridge.context(): Promise<Context>`
Requests the context again (e.g. after a long-running session).

```json
{
  "app_id": "app_531ecbddea174f1d87738613d999577f",
  "app_name": "Syncify",
  "installation_id": "insta_…",
  "environment": "test",
  "merchant_id": "…",
  "locale": "en",
  "host": "https://southbill.com",
  "bridge_version": "1.0"
}
```

Context never contains secrets, API keys, OAuth tokens or the merchant login.

### `bridge.sessionToken(): Promise<{ token, expires_in }>`
Returns a **fresh** short-lived (300 s) HS256 session token for the current installation. Use it whenever your backend needs to re-assert who the merchant is; never cache it beyond its `exp`. Rejects when the app was uninstalled or revoked (`installation_inactive`) — handle that by showing a reconnect hint.

```js
const { token } = await bridge.sessionToken();
await fetch("/api/sync", { headers: { "X-Southbill-Session": token } });
```

### `bridge.resize(height)` / `bridge.autoResize()`
Sets the iframe height, clamped to **400–4000 px**. `autoResize()` observes your document and pushes changes automatically — call it once after `ready()`.

### `bridge.toast(message, variant?)`
Native Southbill toast. `variant` is `success` (default), `error` or `info`. Messages are truncated at **300 characters**.

### `bridge.modal({ title, message, confirmLabel, cancelLabel, variant })`
Host-level confirmation dialog rendered by Southbill (not inside your iframe, so it can overlay the whole dashboard). Resolves `true` / `false`. `variant: "destructive"` renders the confirm action as destructive. `title` max 120 chars, `message` max 300, button labels max 40.

```js
const ok = await bridge.modal({
  title: "Delete mapping?",
  message: "This removes the product link for 42 products.",
  confirmLabel: "Delete",
  variant: "destructive",
});
```

### `bridge.navigate(path)`
Internal Southbill navigation, e.g. `bridge.navigate("/dashboard/orders")`. Only paths starting with `/dashboard` are accepted; absolute URLs, protocol-relative paths and anything else are rejected with an error.

### `bridge.open(url)`
Opens an **https** URL in a new browser tab (`noopener,noreferrer`). Use for documentation, your own dashboard, support pages.

### `bridge.redirect(url)`
Full **top-level** redirect away from the dashboard. Use only when the merchant is intentionally leaving Southbill.

### `bridge.authorize(url, mode?)`
**The way to run OAuth or any third-party consent flow.** `mode` is `"popup"` (default) or `"redirect"`. Southbill performs the navigation at browser top level; if the popup is blocked it falls back to a top-level redirect.

```js
await bridge.authorize(
  "https://accounts.example.com/oauth/authorize?client_id=…&redirect_uri=https://app.example.com/callback&state=…"
);
```

Your own callback URL finishes the flow on your backend (store the result against `installation_id`), then the merchant returns to the embedded app. Never render an external authorization page inside the iframe — providers block framing and the sandbox forbids top-level navigation from your page.

### `bridge.close()`
Leaves the embedded surface and returns the merchant to *Dashboard → Apps*.

## 4. Wire protocol (raw postMessage)

The SDK is optional. The protocol is stable and documented:

```
request : { type: "southbill:<action>", id?, ...payload }   -> posted to `host`
success : { type: "southbill:<action>:result", id, ok: true, ...data }
failure : { type: "southbill:<action>:error",  id, ok: false, message }
announce: { type: "southbill:ready", version, context }
```

Supported actions: `ping`, `context`, `session-token`, `resize`, `toast`, `modal`, `navigate`, `open`, `redirect`, `authorize`, `close`. Anything else is silently ignored.

```js
const host = new URLSearchParams(location.search).get("host");
parent.postMessage({ type: "southbill:resize", height: document.body.scrollHeight }, host);
```

**Backwards compatibility:** the legacy id-less messages `southbill:resize`, `southbill:toast`, `southbill:navigate` and `southbill:session-token` keep working exactly as before. Existing embedded apps need no changes.

## 5. Security model

| Guarantee | Detail |
| --- | --- |
| Origin binding | Only messages whose `event.origin` equals the exact origin of your saved `embedded_url` are processed |
| Window binding | Only the actual iframe `contentWindow` is accepted — other frames or tabs cannot impersonate your app |
| Action allowlist | Unknown `type` values and malformed payloads are dropped without a reply |
| Navigation | `navigate` is restricted to `/dashboard/*`; no open redirects |
| External URLs | `open`, `redirect` and `authorize` require `https:` |
| Bounded input | Text truncated (300 chars), height clamped (400–4000 px) |
| No secret exposure | The host never returns Southbill API keys, OAuth client secrets or the merchant session. The only credential handed out is the 5-minute session token |
| Replies | Responses are posted back only to your exact app origin |

On your side: verify the session token server-side (signature, `aud`, `iss`, `exp`/`nbf`), never trust values that arrived through the iframe URL without verification, and set `Content-Security-Policy: frame-ancestors https://southbill.com`.

## 6. Errors

Bridge calls reject with an `Error`. Common messages:

| Message | Cause |
| --- | --- |
| `Bridge timeout: <action>` | No answer in 15 s — usually the wrong `host` origin |
| `Only /dashboard paths are allowed` | `navigate` received an external or malformed path |
| `An https URL is required` | `open` / `redirect` / `authorize` got a non-https URL |
| `Invalid height` | `resize` received a non-numeric height |
| `Context unavailable` | The host session is not ready yet — await `ready()` first |
| `installation_inactive` | The merchant uninstalled or revoked the app |

## 7. Minimal example

```html
<script src="https://www.southbill.com/southbill-app-bridge.js"></script>
<script>
  (async () => {
    const bridge = SouthbillAppBridge.create();
    const ctx = await bridge.ready();
    bridge.autoResize();

    if (ctx.environment === "test") bridge.toast("Sandbox mode", "info");

    document.querySelector("#connect").onclick = () =>
      bridge.authorize("https://accounts.example.com/oauth/authorize?client_id=…");

    document.querySelector("#orders").onclick = () =>
      bridge.navigate("/dashboard/orders");
  })();
</script>
```

