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

# Known limitations

> Current rough edges in the SDK, with workarounds where available.

These are documented rough edges rather than surprises — hitting one and not knowing whether it's you or us costs debugging time.

## No client-side egress enforcement

**`ctx.fetch` is server-side only, and nothing enforces your `egress` list in the browser.** `useIntegrationFetch()` from `@ekanos/sdk/hooks` is the fetch a widget's query functions should take, so the same query function can run against `ctx.fetch` on the server and this hook's value on the client. Outside a mounted `IntegrationFetchProvider` it is a thin `globalThis.fetch` passthrough with no allowlist check:

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

function useData() {
  const fetch = useIntegrationFetch();

  return useQuery({
    queryKey: ['data'],
    queryFn: async () => {
      const response = await fetch('https://api.example.com/data');
      return response.json();
    },
  });
}
```

For anything that needs credentials or the allowlist, do the vendor call server-side (a schedule or `onActivate` writing `clientReadable` storage, or an action) and read the result from the widget.

## OAuth gaps

The host runs the partner OAuth flow, and the harness runs a local loop in live mode (see [Live mode](/harness/live-mode)). Gaps that remain:

* No host-driven token refresh
* No partner disconnect hook
* Token-endpoint client authentication is HTTP Basic or `client_secret_post` only (no `private_key_jwt`)
* No per-account secret path for OAuth-form integrations

`onTokens` is an ordinary async function, so you can also unit-test it directly:

```ts theme={null}
const ctx = createMockContext({ integration: { slug: 'acme-payments' } });

await acmeOAuth.onTokens(ctx, {
  accessToken: 'test_token',
  expiresAt: new Date(Date.now() + 3_600_000).toISOString(),
});
```

## useActivateIntegration needs the host's provider

**`useActivateIntegration()` and `useDeactivateIntegration()` read the host's server actions from `IntegrationActivationProvider`.** Nothing warns you at build time; outside a provider, calling the mutation rejects with:

```
No IntegrationActivationProvider is mounted. The host application must bind its activation server actions (see @ekanos/sdk/hooks IntegrationActivationProvider) above any component that activates integrations.
```

**Workaround:** Mount the provider yourself in tests and Storybook:

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

const mockActions = {
  activate: async () => ({ success: true }),
  deactivate: async () => ({ success: true }),
};

<IntegrationActivationProvider actions={mockActions}>
  <MyActivationForm accountId="acc_123" productSlug="acme-payments" />
</IntegrationActivationProvider>;
```

The host mounts it in production.

## Storage schema evolution is manual

**A stored value that no longer matches its schema throws `StorageValidationError` on read.** The platform does not migrate rows for you.

**Workaround:** Version the key or widen with `z.union`:

```ts theme={null}
// Wrong — existing rows fail the new required field
'settings/location': z.object({ label: z.string(), newField: z.string() }),

// Right — a versioned key
'settings/location-v2': z.object({ /* new shape */ }),

// Also right — accept both shapes
'settings/location': z.union([
  z.object({ label: z.string(), newField: z.string() }),
  z.object({ label: z.string() }),
]),
```

## ctx.storage.user throws for machine actors

**Every `ctx.storage.user` call throws when `ctx.actor.kind` is `'machine'`** — there is no user to scope the row to.

**Workaround:** Use account scope for anything a workflow might touch:

```ts theme={null}
storage: {
  account: {
    settings: z.object({ /* … */ }),              // usable by machine actors
  },
  user: {
    'preferences/units': z.enum(['c', 'f']),     // throws for machine actors
  },
}
```

## Widget sizing declared twice

**`layouts` on the widget declaration drives the real dashboard; the dev harness reads its own `width` (`'half'` or `'full'`) from the harness config instead.** Neither validates the other, so changing one does not change the other.

## Slug written in two places

**Your slug appears in both `ekanos.json` and the definition.** `ekanos validate` compares them and reports a `project.slug-agreement` finding if they disagree.

```json theme={null}
{ "slug": "acme-payments" }
```

```ts theme={null}
export const integration = defineIntegration({
  slug: 'acme-payments',
  // …
});
```

The mismatch is caught rather than prevented, and the slug addresses your harness route, product, widget-id prefix, and tool namespace — fix it early.

## Next steps

* [Integration shape](/sdk/integration-shape) — full reference
* Report new issues to `npm@govastly.com`


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