Session & bridge
Connect to the parent portal and read the session: token, config, theme, and language.
The bridge API is how your block connects to the portal and learns about its environment. The center of it is the session: a single object describing the authenticated context your block runs in.
initialize(options?)
Performs the postMessage handshake with the parent portal and resolves to a
SparkSession. Call it once at startup, before any
entity or use-case call.
import { initialize } from '@epilot/spark-sdk'
const session = await initialize()Key behaviors:
- Idempotent: repeat calls return the cached session; concurrent calls share one in-flight handshake. You can call it from multiple modules without coordinating.
- Rejects on timeout: if the portal doesn't respond within
timeout(default 5000ms) it throws aSparkTimeoutError. A failed init clears the cache, so a later retry will attempt a fresh handshake. - Side effects on success: injects the portal's theme CSS variables, sets the light/dark class, and subscribes to future theme, locale, and viewport updates.
InitOptions
| Option | Type | Default | Description |
|---|---|---|---|
timeout | number | 5000 | Milliseconds to wait for the portal's init response before rejecting. |
apiBaseUrl | string | from portal | Override the API base URL. By default it's taken from the portal's init response. |
allowedParentOrigins | string[] | [] | Origins the bridge will trust. When set, inbound messages from other origins are dropped and outbound messages target the single configured origin instead of *. Empty = accept any parent (legacy). |
const session = await initialize({
timeout: 8000,
allowedParentOrigins: ['https://portal.epilot.io'],
})Security tip: in production, pass
allowedParentOriginsso the bridge only trusts the real portal origin. Without it, the bridge accepts messages from any parent and posts to*.
getSession()
Returns the current session with a freshly-fetched auth token. The token is
requested from the portal on every call and never cached on the block side, so
each returned session carries a current token. Rejects with
SparkNotInitializedError if called before initialize() has
resolved. Use it in code paths that run only after startup (most of your app).
import { getSession } from '@epilot/spark-sdk'
const { token, apiBaseUrl } = await getSession()In React, prefer reading the session from context (see Getting started) so components re-render when it's ready.
isInitialized()
Returns true once a session exists. Handy for guards that shouldn't throw.
import { isInitialized } from '@epilot/spark-sdk'
if (isInitialized()) {
// safe to call getSession()
}The SparkSession object
interface SparkSession {
/** Portal auth token for API calls */
token: string
/** Portal API base URL */
apiBaseUrl: string
/** User's language preference */
lang?: string
/** Organization ID */
orgId?: string
/** Portal ID */
portalId?: string
/** CSS variables string for theming (injected into document head) */
cssVariables?: string
/** Current theme mode */
theme?: 'light' | 'dark'
/** App-level configuration from the installation (e.g., integration_id) */
config?: Record<string, unknown>
}The SDK keeps theme and lang on the cached session in sync as the portal
pushes updates, so reading (await getSession()).lang always reflects the current
language.
config: your installation settings
config is the per-installation configuration the portal admin set up for your
block. Its shape is up to your block; cast the fields you expect. A HEMS
reads its Integration Hub id, target entities, and a mock-mode flag:
// app/providers/block-context-provider.tsx
const session = useSparkSession()
const integrationId = session.config?.integration_id as string | undefined
const entityIds = (session.config?.entityIds ?? {}) as Record<string, string>
const assetId = entityIds.asset
const isMockedMode = session.config?.is_mocked_mode === trueintegrationId is the input every use-case call needs;
isMockedMode is how the block renders mock data in the block editor without
calling real integrations.
Reporting content size
The portal sizes your iframe from what you report: it can't measure across the
origin boundary. Call these whenever your layout changes (data loads, routes
switch, panels expand). See
Getting started for the
ResizeObserver pattern.
updateContentHeight
updateContentHeight(height: number): voidimport { updateContentHeight } from '@epilot/spark-sdk'
updateContentHeight(document.body.scrollHeight)updateContentSize
Reports width and/or height. Use this for footer blocks or absolutely-positioned content where the iframe must match the full dimensions.
updateContentSize(size: { height?: number; width?: number }): voidimport { updateContentSize } from '@epilot/spark-sdk'
updateContentSize({
width: document.documentElement.scrollWidth,
height: document.documentElement.scrollHeight,
})