Skip to main content
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:
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). 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:

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:
Workaround: Mount the provider yourself in tests and Storybook:
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:

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:

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