> ## Documentation Index
> Fetch the complete documentation index at: https://sdk.ekanos.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Live Mode

> Run real requests against your own API with a localhost OAuth loop and live credentials.

Live mode lets you test your integration against real vendor APIs while keeping Ekanos mocked. The toolbar's **Data** dropdown switches between **Fixtures** and **Live API**; in live mode, the harness stops seeding your data keys, your widgets' own query functions run, and their requests reach your actual API under your declared egress.

## Setup

Declare a `live` block on your integration entry:

```ts theme={null}
{
  slug: 'my-integration',
  name: 'My Integration',
  // ... other fields ...

  definition: asHarnessDefinition(myDefinition),

  live: {
    // Optional: seeds that stay mocked in live mode
    seeds: [
      { queryKey: settingsKey, data: mySettings },
    ],
    // Optional: open in live mode until the toolbar is changed
    default: 'live',
  },
}
```

Now the toolbar's **Data** dropdown's **Live API** option is enabled for this integration.

## Egress validation

**The harness enforces your definition's `egress` allowlist in live mode.** A browser request to an undeclared origin throws `EgressDeniedError` (from `@ekanos/sdk/context`, `code: 'EGRESS_DENIED'`):

```
Egress to https://unlisted.example.com is not in this integration's allowlist. Declare the origin in the integration's egress list — an exact origin ("https://api.example.com") or a "*." subdomain wildcard ("https://*.example.com") — or call an allowed origin instead. Allowlist changes are a security-posture change reviewed at promotion.
```

Allowed requests are sent with `redirect: 'error'`, so a redirect to another origin fails rather than being followed.

This is a harness convenience, not a preview of production enforcement: production runs no egress check on the client path today. Flipping to live mode is still a real test of the declaration under review, not of a second list kept beside it.

<Warning>
  **Do NOT set `live.egress` if `definition` is set.** Supplying both is a hard
  error naming your slug. The harness reads the allowlist from your definition,
  so there is only one list and it is the one a reviewer reads. `live.egress`
  exists only for an entry with no `definition`.
</Warning>

## Credential storage

Live mode reads admin-tier credentials (the OAuth client id and secret, and signed webhooks' secrets) from `.ekanos/local-secrets.json` in your project root (gitignored, never packed by `ekanos publish`, read on every request). The harness generates the file on first use. OAuth tokens are not written to it.

**On the Activation or Triggers surface:**

1. Find the **Local OAuth** panel (shown for integrations that declare `oauth`, in either mode)
2. Follow the checklist to create a developer app at your vendor's console
3. Paste the client id, secret, and any webhook signing secrets into the form (one field per secret name your definition declares)
4. Click **Save credentials**

The shell writes them to `.ekanos/local-secrets.json` and never echoes values back. A saved key shows as `filled` beside an empty field; a blank field keeps what is saved. Saving, or editing a value in the file, rebuilds the live context and forgets any current connection.

<Note>
  The OAuth connection (tokens) lives in dev-server memory only: restarting the
  dev server forgets it and you Connect again. Activation data submitted through
  a plain activation form is also held in memory only, never written to
  `localStorage`, a fixture, or the registry, and is cleared on reload and on
  Disconnect.
</Note>

## Opting in

An integration with no `live` block is fixtures-only; the toolbar control is disabled for it and says why. The harness also forces fixtures mode while a `live.egress` is wider than the definition's `egress`.

One WITH a `live` block opens in fixtures mode by default. To use live mode:

1. Select **Live API** in the toolbar's **Data** dropdown, or
2. Declare `live: { default: 'live' }` — for integrations whose widgets never call the vendor from the browser (every vendor call is server-side behind `ctx.fetch`)

A choice made in the toolbar wins over `live.default` and is saved in `localStorage`, so it survives reloads.

## Keys that stay mocked (seeds)

`live.seeds` lists the react-query keys that stay mocked in live mode — Ekanos-side state your widgets read before they can call anything (a saved setting, an account preference). Everything not listed runs its real query function:

```ts theme={null}
live: {
  seeds: [
    { queryKey: settingsKey, data: { locale: 'en', timezone: 'UTC' } },
  ],
}
```

This is the boundary the design draws:

* **Mocked:** Ekanos-side state
* **Live:** your vendor

A query key with a seed never fetches, so an HTTP fixture behind it is unreachable. Use one or the other.

## OAuth loop on localhost

For an integration with `oauth` declared:

<Steps>
  <Step>
    Switch to **Live API** (the entry needs a `live` block).
  </Step>

  <Step>
    On the Activation or Triggers surface, find the **Local OAuth** panel.
  </Step>

  <Step>
    Follow the checklist to set up a developer app at your vendor's console. It is derived from your declaration: the redirect URI `http://localhost:3100/api/integrations/<slug>/oauth/callback` on the default port (Copy button), the declared scopes, and the code grant (with PKCE when declared). Add `oauth.setup` to the entry for the console link and key names it cannot know.
  </Step>

  <Step>
    Paste the client id, secret, and any webhook signing secrets into the form and
    click **Save credentials**.
  </Step>

  <Step>
    Click **Connect** in the activation dialog. The shell's `oauth/connect` route
    builds the authorize URL (PKCE when declared) and sends the browser to your
    vendor.
  </Step>

  <Step>
    Your vendor redirects back to `oauth/callback`, which verifies the single-use
    `state`, exchanges the code through `ctx.fetch` (an undeclared token-endpoint
    origin fails with `EgressDeniedError`), and awaits your `onTokens` handler on
    the live context.
  </Step>

  <Step>
    `oauth/status` then answers connected, the SDK's `OAuthActivationForm`
    auto-activates, and `onActivate` runs on the same context.
  </Step>

  <Step>
    Your widgets read what the handlers wrote through `fetchIntegrationStorage()`, unchanged. An empty read runs your declared schedules once first, as the host does.
  </Step>
</Steps>

The connection is **memory-only**. Restarting the dev server forgets it.

The API routes refuse any request whose `Host` is not loopback unless `EKANOS_HARNESS_EXPOSED=1`; `ekanos dev --host` sets that after printing a warning.

## API routes available

The shell's API route (`createHarnessApiHandlers` from `@ekanos/harness/api`) serves these under `/api/integrations/<slug>/`:

| Route | Method | Purpose |
| - | - | - |
| `oauth/connect` | GET | Build the authorize URL and redirect to the vendor |
| `oauth/callback` | GET | Verify `state`, exchange the code, run `onTokens`, redirect back (to `/<slug>/activation` by default) |
| `oauth/status` | GET | `{ success: true, data: { obtainedAt } }` when connected, otherwise `{ success: false, error: 'Not connected' }` |
| `storage` | GET | Read client-readable storage (what `fetchIntegrationStorage()` calls) |
| `trigger` | POST | Run a webhook, a schedule, or `onActivate` for the Triggers and Activation surfaces |
| `local-setup` | GET, PUT | Read which secrets are filled; save credentials to `.ekanos/local-secrets.json` |
| `actions/<actionName>` | POST | Run a declared action (see [Actions](/harness/actions)) |

In live mode the browser passes the active integration's requests to these routes. `local-setup` and `actions/<actionName>` are reached in both modes. The routes run against one **server-held context per integration**, whose `ctx.fetch` reaches your vendor under the declared egress.

## Widget data fetching

Use `useIntegrationFetch()` from `@ekanos/sdk/hooks`. The harness mounts `IntegrationFetchProvider` and `IntegrationActivationDataProvider` unconditionally, so the hook returns a fetch already gated for whichever mode the toolbar is in, and `useActivationData()` returns what was last submitted to the activation form (`null` before any submission or after Disconnect):

```tsx theme={null}
import type { IntegrationFetch } from '@ekanos/sdk';
import { useIntegrationFetch } from '@ekanos/sdk/hooks';
import { useQuery } from '@tanstack/react-query';

export async function fetchMyData(fetch: IntegrationFetch, accountId: string) {
  const response = await fetch('https://api.acme.com/v1/data');
  if (!response.ok) throw new Error(`Acme returned ${response.status}`);
  return response.json();
}

function useMyData(accountId: string) {
  const fetch = useIntegrationFetch();

  return useQuery({
    queryKey: ['my-data', accountId],
    queryFn: () => fetchMyData(fetch, accountId),
  });
}
```

In fixtures mode, `fetch` is answered from your fixtures. In live mode, it reaches your real API.

<Note>
  Taking an `IntegrationFetch` as an argument lets the same query function serve
  `ctx.fetch` on the server and this hook's fetch on the client. In the harness
  the client fetch is gated by the harness's own check; in production today it
  is a plain, unmediated browser fetch.
</Note>

The `live.FetchProvider` field is deprecated. It still works and receives `fetch`, `mode`, and `activationData` as props; use it only to compose an authenticated fetch once at a single seam.

## Knowing which mode is active

Call `useHarnessDataMode()` from `@ekanos/harness/hooks`. It has no SDK equivalent, so call it only from code that runs nowhere but the harness:

```tsx theme={null}
import { useHarnessDataMode } from '@ekanos/harness/hooks';
import { useIntegrationFetch } from '@ekanos/sdk/hooks';

function MyWidget() {
  const mode = useHarnessDataMode(); // 'fixtures' | 'live'
  const fetch = useIntegrationFetch();

  if (mode === 'live') {
    // Render live-mode-only UI
  }
}
```

It returns the **EFFECTIVE** mode—accounting for an integration with no `live` block or a `live.egress`/definition mismatch (both force fixtures regardless of the toolbar).

## Troubleshooting

### EgressDeniedError in live mode

The origin is not in your definition's `egress`. Add it or fix a typo in your URL.

### NoRecordedResponseError in live mode

The request was a same-origin `/api/…` request outside the active integration's own `/api/integrations/<slug>/…` routes. Those are answered from fixtures in both modes; record a fixture for it, or call your vendor with an absolute URL.

### Can't find where credentials were saved

Check `.ekanos/local-secrets.json` in your project root. The file is gitignored and never committed.

### Changing credentials

Live connections are memory-only and forgotten on restart. To change credentials, save new values in the **Local OAuth** panel or edit `.ekanos/local-secrets.json`; either forgets the current connection.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.