npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@epilot/spark-sdk

v1.0.0-alpha.2

Published

SDK utilities for Spark

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

Quick 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