@epilot/spark-sdk
v1.0.0-alpha.2
Published
SDK utilities for Spark
Maintainers
Keywords
Readme
@epilot/spark-sdk
SDK for building custom portal blocks that run within the epilot customer portal.
Installation
npm install @epilot/spark-sdk
# or
bun add @epilot/spark-sdkQuick Start
import { initialize, getEntity, searchEntities } from '@epilot/spark-sdk'
// Initialize the SDK (connects to parent portal via postMessage)
const session = await initialize()
console.log('Connected to portal:', session.portalId)
// Fetch a single entity
const contract = await getEntity({
slug: 'contract',
entity_id: '5da0a718-c822-403d-9f5d-20d4584e0528',
})
// Search entities
const { results, hits } = await searchEntities({
slug: 'order',
q: 'pending',
size: 10,
})API Reference
Initialization
initialize(options?): Promise<SparkSession>
Initializes the SDK and establishes connection with the parent portal. Returns a session containing the authentication token and portal configuration.
Safe to call multiple times - subsequent calls return the cached session.
interface InitOptions {
timeout?: number // Timeout in ms (default: 5000)
apiBaseUrl?: string // Override the API base URL
}
interface SparkSession {
token: string // Portal auth token
apiBaseUrl: string // API endpoint URL
lang?: string // User's language
orgId?: string // Organization ID
portalId?: string // Portal ID
}Example:
const session = await initialize({ timeout: 10000 })
console.log('Token:', session.token)
console.log('Language:', session.lang)getSession(): SparkSession
Returns the current session. Throws SparkNotInitializedError if called before initialize().
const { token, apiBaseUrl } = getSession()isInitialized(): boolean
Check if the SDK has been initialized.
if (!isInitialized()) {
await initialize()
}Entity API
getEntity(params): Promise<EntityItem | undefined>
Fetches a single entity for the portal user.
interface EntityGetParams {
slug: string // Entity schema (e.g., 'contract', 'order')
entity_id?: string // Entity ID to fetch
hydrate?: boolean // Resolve nested relations
fields?: string[] // Fields to include
templates?: Record<string, string> // Template strings for synthetic fields
}Example:
const contract = await getEntity({
slug: 'contract',
entity_id: '5da0a718-c822-403d-9f5d-20d4584e0528',
hydrate: true,
fields: ['_id', '_title', 'customer', 'status'],
})
console.log(contract?._title)searchEntities(params): Promise<EntityResponseWithHits>
Searches entities for the portal user.
interface EntitySearchParams {
slug: string | string[] // Single or multiple entity schemas
q?: string // Search query
q_fields?: string[] // Fields to search in
sort?: string // Sort order (e.g., '_created_at:desc')
from?: number // Pagination offset
size?: number // Page size (max 1000)
hydrate?: boolean // Resolve nested relations
fields?: string[] // Fields to include
filters?: Array<Record<string, unknown>> // Additional filters
}
interface EntityResponseWithHits {
results?: EntityItem[]
hits?: number
pagination?: {
from?: number
size?: number
total?: number
has_more?: boolean
}
}Example:
const { results, pagination, hits } = await searchEntities({
slug: 'contract',
q: 'active',
size: 10,
sort: '_created_at:desc',
fields: ['_id', '_title', 'status'],
})
console.log(`Found ${hits} contracts`)
results?.forEach((contract) => {
console.log(contract._title)
})Advanced: Direct Client Access
For operations not covered by getEntity and searchEntities, you can access the full customer-portal-client:
import { initialize, getPortalClient } from '@epilot/spark-sdk'
await initialize()
const client = getPortalClient()
// Access any customer-portal-client method
const files = await client.getAllFiles({ origin: 'END_CUSTOMER_PORTAL' })Custom Events
on(event, handler): Unsubscribe
Subscribe to custom events from the parent portal.
const unsubscribe = on('custom-event', (data) => {
console.log('Received:', data)
})
// Later: cleanup
unsubscribe()send(event, data?): void
Send a custom message to the parent portal.
send('custom-event', { action: 'save', value: 42 })updateContentHeight(height): void
Update the content height reported to the parent portal. The portal uses this to resize the iframe appropriately. Call this whenever your content height changes.
// After rendering content
updateContentHeight(document.body.scrollHeight)
// Or with a specific element
const container = document.getElementById('app')
updateContentHeight(container.scrollHeight)
// Or with ResizeObserver
// With ResizeObserver for dynamic content
const observer = new ResizeObserver((entries) => {
updateContentHeight(entries[0].contentRect.height)
})
observer.observe(document.getElementById('app'))React Example with ResizeObserver:
import { useEffect, useRef } from 'react'
import { updateContentHeight } from '@epilot/spark-sdk'
function App() {
const containerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const container = containerRef.current
if (!container) return
const observer = new ResizeObserver(() => {
updateContentHeight(container.scrollHeight)
})
observer.observe(container)
return () => observer.disconnect()
}, [])
return <div ref={containerRef}>{/* Your content */}</div>
}React Example
import { useEffect, useState } from 'react'
import {
initialize,
getEntity,
SparkSession,
EntityItem,
} from '@epilot/spark-sdk'
function ContractDetails({ contractId }: { contractId: string }) {
const [session, setSession] = useState<SparkSession | null>(null)
const [contract, setContract] = useState<EntityItem | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
initialize().then(setSession)
}, [])
useEffect(() => {
if (session && contractId) {
setLoading(true)
getEntity({
slug: 'contract',
entity_id: contractId,
hydrate: true,
})
.then(setContract)
.finally(() => setLoading(false))
}
}, [session, contractId])
if (!session) return <div>Connecting to portal...</div>
if (loading) return <div>Loading contract...</div>
if (!contract) return <div>Contract not found</div>
return (
<div>
<h1>{contract._title}</h1>
<p>Status: {contract.status}</p>
</div>
)
}Error Handling
The SDK provides custom error classes for specific error scenarios:
import { SparkError, SparkTimeoutError, SparkNotInitializedError } from '@epilot/spark-sdk'
try {
await initialize({ timeout: 1000 })
} catch (error) {
if (error instanceof SparkTimeoutError) {
console.log(`Initialization timed out after ${error.timeout}ms`)
} else if (error instanceof SparkNotInitializedError) {
console.log('SDK not initialized')
} else if (error instanceof SparkError) {
console.log('SDK error:', error.message)
}
}Portal-Side Requirements
For the SDK to work, the parent portal must implement the message handler:
window.addEventListener('message', (event) => {
if (event.data?.source === 'spark-bridge' && event.data?.event === 'spark-bridge:init') {
event.source?.postMessage(
{
event: 'spark-bridge:init',
source: 'portal',
token: currentUserToken,
apiBaseUrl: 'https://portal-api.epilot.io',
lang: userLanguage,
orgId: organizationId,
portalId: portalId,
},
event.origin
)
}
})License
MIT
