@fiction/sdk
v1.0.125
Published
SDK for Fiction app authentication and user management
Readme
Fiction SDK
Lightweight client library for Fiction digital agent integration. Built for Fiction-owned properties (app, www, widget).
Installation
npm install @fiction/sdkQuick Start
import { FictionSDK } from '@fiction/sdk'
// Singleton pattern - shares state across your app
const sdk = FictionSDK.getInstance({ isDev: true })
// Get public digital agent
const agent = await sdk.getPublicAgent({ handle: 'andrew' })
// Authenticate user
await sdk.requestAuthCode({ email: '[email protected]' })
await sdk.loginWithCode({ email: '[email protected]', code: '123456' })
// Access reactive state
console.log(sdk.activeUser.value)
console.log(sdk.currentAgent.value)
console.log(sdk.currentOrg.value)Core Philosophy
Minimal API Surface: Clean wrapper methods hide internal complexity. No direct apiClient exposure prevents type generation issues.
Agent-First Architecture: Digital agents are primary entities. Organization context derived from agent, not passed redundantly.
Singleton by Default: Browser-only singleton prevents duplicate API calls and separate reactive state. Multiple getInstance() calls return same instance.
SSR-Safe: No singleton in Node.js—each request gets new instance to prevent request bleeding.
Public API
Authentication
// Request email verification code
await sdk.requestAuthCode({ email: '[email protected]' })
// Login with code
await sdk.loginWithCode({
email: '[email protected]',
code: '123456'
})
// Get current user
const user = await sdk.getCurrentUser()
// Logout
await sdk.logout()Digital Agent Access
// Public agent by handle (no auth)
const agent = await sdk.getPublicAgent({ handle: 'andrew' })
// Public agent by email (no auth)
const agent = await sdk.getAgentByEmail({ email: '[email protected]' })Usage Tracking
// Unified tracking method
await sdk.trackUsage({
agentId: 'agt_456', // Agent contains orgId
type: 'voice', // 'voice' | 'chat'
quantity: 45, // Seconds for voice, characters for chat
participantId: 'usr_123' // Optional: conversation participant
})Reactive State
// Core state
sdk.activeUser.value // EnrichedUser | undefined
sdk.token.value // string | null
sdk.loading.value // boolean
sdk.error.value // string | null
// Computed properties (auto-derived from activeUser)
sdk.currentAgent.value // Agent | undefined
sdk.currentOrg.value // OrgInfo | undefinedSingleton Pattern
Why Singleton: Prevents duplicate API requests and maintains shared reactive state across your application.
// Both return same instance
const sdk1 = FictionSDK.getInstance({ isDev: true })
const sdk2 = FictionSDK.getInstance({ isDev: true })
console.log(sdk1 === sdk2) // true
// Reactive state shared
sdk1.token.value = 'test-token'
console.log(sdk2.token.value) // 'test-token'Cross-Bundle Singleton: Uses globalThis to share instance between separate bundles (www + widget).
// Global SDK (www.fiction.com)
window.fictionSDK = FictionSDK.getInstance({ isDev: true })
// Widget automatically uses singleton
const widget = new FictionWidget({ handle: 'andrew' })
console.log(window.fictionSDK === widget.sdk) // trueTesting Pattern: Use clear() to reset singleton between tests.
import { afterEach, it, expect } from 'vitest'
afterEach(() => {
const sdk = FictionSDK.getInstance({ isDev: true })
sdk.clear() // Clears session + state + destroys singleton
})
it('should test singleton behavior', () => {
const sdk = FictionSDK.getInstance({ isDev: true })
// Test runs with clean state
})Environment Detection: apiBase auto-detected from environment. Only override for testing.
// ✅ Normal usage (95%+ cases) - apiBase omitted
const sdk = FictionSDK.getInstance({ isDev: true })
// Dev: http://localhost:5555 (auto)
// Prod: https://app.fiction.com (auto)
// ✅ Testing only - custom apiBase
const testSDK = FictionSDK.getInstance({
isDev: true,
apiBase: 'http://localhost:9999'
})Usage Patterns
Blog Author Attribution
// Astro component (www.fiction.com)
import { FictionSDK } from '@fiction/sdk'
const sdk = FictionSDK.getInstance({ isDev: import.meta.env.DEV })
const authorAgent = await sdk.getAgentByEmail({ email: '[email protected]' })
// Use digital agent data instead of static strings
const authorAvatar = authorAgent?.avatar?.url
const authorName = authorAgent?.name
const authorBio = authorAgent?.summaryWidget Integration
import { FictionWidget } from '@fiction/sdk/widget'
const widget = new FictionWidget({
mode: 'modal', // 'inline' | 'popup' | 'modal'
handle: 'andrew', // Or provide agent object directly
context: 'support', // Optional: agent context override
firstMessage: 'Hi!' // Optional: custom greeting
})
// Update without re-mount
widget.update({
agent: newAgent,
context: 'sales'
})
// Cleanup
widget.destroy()Profile Page
import { AgentChat } from '@fiction/sdk/agent'
const sdk = FictionSDK.getInstance({ isDev: true })
const agent = await sdk.getPublicAgent({ handle: 'andrew' })<template>
<AgentChat
:sdk="sdk"
:agent="agent"
theme-color="#3b82f6"
/>
</template>Error Handling
All wrapper methods return undefined on error (no exceptions thrown). Check reactive error state for details.
const agent = await sdk.getPublicAgent({ handle: 'nonexistent' })
if (!agent) {
console.error(sdk.error.value) // 'Agent not found'
}Build Strategy
SDK Build: ES modules, tree-shakeable, externals (Vue, ElevenLabs)
- Format: ES modules
- Bundle size: < 256KB
- Build time: 5 seconds
Widget Build: IIFE, single-file CDN bundle
- Format: IIFE (global
window.FictionWidget) - Bundle size: < 1MB
- Includes all dependencies
Import Patterns
// ✅ Correct - Use bundled entry points
import { FictionSDK } from '@fiction/sdk'
import { AgentChat, AgentProvider } from '@fiction/sdk/agent'
import { FictionWidget } from '@fiction/sdk/widget'
import { getDemoAgents } from '@fiction/sdk/demo'
// ❌ Wrong - Deep imports don't work
import { FictionSDK } from '@fiction/sdk/FictionSDK'
import AgentChat from '@fiction/sdk/agent/ui/AgentChat.vue'Package Exports
{
"exports": {
".": {
"import": "./dist/sdk.js",
"types": "./dist/sdk.d.ts"
},
"./agent": {
"import": "./dist/agent.js",
"types": "./dist/agent.d.ts"
},
"./widget": {
"import": "./dist/widget.js",
"types": "./dist/widget.d.ts"
},
"./demo": {
"import": "./dist/demo.js",
"types": "./dist/demo/index.d.ts"
}
}
}Agent-First Architecture
Digital agents are primary entities that determine organizational context.
// ✅ Agent contains all context
await sdk.trackUsage({
agentId: 'agt_456', // Includes orgId internally
type: 'voice',
quantity: 45
})
// ❌ Don't pass redundant orgId
await sdk.trackUsage({
orgId: 'org_123', // Redundant - derived from agentId
agentId: 'agt_456',
type: 'voice',
quantity: 45
})Benefits:
- Simpler APIs (fewer parameters)
- Single source of truth (agent determines org)
- Automatic context resolution
- Consistent across all SDK methods
Reactive State Pattern
SDK maintains reactive state that auto-updates components.
const sdk = FictionSDK.getInstance({ isDev: true })
// Core refs (direct state)
sdk.activeUser.value // Updated by auth operations
sdk.token.value // Updated by login/logout
sdk.loading.value // Updated during API calls
sdk.error.value // Updated on errors
// Computed properties (auto-derived)
sdk.currentAgent.value = computed(() => {
const user = sdk.activeUser.value
if (!user?.agents) return undefined
const agentId = user.primaryAgentId || user.agents[0]?.agentId
return user.agents.find(a => a.agentId === agentId)
})
sdk.currentOrg.value = computed(() => {
const agent = sdk.currentAgent.value
if (!agent?.orgId) return undefined
return sdk.activeUser.value?.orgs.find(org =>
org.orgId === agent.orgId
)
})Benefits:
- Components auto-update when user changes
- No manual state management needed
- Consistent with Fiction app patterns
- Agent-first architecture (org derived from agent)
Component Integration
Prop-Based Dependency Injection
SDK components accept sdk prop instead of using global services.
<script setup>
import { FictionSDK } from '@fiction/sdk'
import { AgentChat } from '@fiction/sdk/agent'
const sdk = FictionSDK.getInstance({ isDev: true })
const agent = await sdk.getPublicAgent({ handle: 'andrew' })
</script>
<template>
<AgentChat
:sdk="sdk"
:agent="agent"
theme-color="#3b82f6"
/>
</template>Available Components
AgentChat - Main agent interface (chat/voice) AgentProvider - Handle → agent resolver wrapper ElAgentAbout - Profile information display ElAgentChat - Text chat interface ElAgentVoice - Voice call interface ElAgentSidebar - Navigation sidebar
Demo Data
Static demo agents for marketing/showcase.
import { getDemoAgents, getDemoAgentByHandle } from '@fiction/sdk/demo'
// All demo agents
const demoAgents = getDemoAgents()
// Specific demo agent
const andrew = getDemoAgentByHandle('andrew')Single Source of Truth: Demo data maintained in Fiction app at src/modules/agent/static/data.ts, bundled into SDK at build time.
Security Model
Public-Only Access: getPublicAgent() and getAgentByEmail() only return agents with visibility: 'public'.
Authentication Required: User operations require valid JWT token in sdk.token.value.
Rate Limiting: Widget includes 10 comments/hour per agent, 100 likes/hour (future).
Performance Optimization
Single Query Pattern: Public endpoints use single SQL query with LEFT JOINs for media.
Voice Recordings Excluded: Public endpoints exclude voice recordings (performance).
Build Time Bundling: App dependencies (@/ imports) bundled during SDK build.
Ultra-Fast Builds: 5-second builds via custom tsconfig + esbuild optimization.
Type Safety
Full TypeScript Inference: From API routes to SDK methods to component props.
Wrapper Method Pattern: Clean function signatures serialize perfectly to .d.ts files.
No Complex Types: Simple Promise<Agent | undefined> instead of Hono RPC types.
// Clean wrapper method
async getPublicAgent(args: {
handle: string
}): Promise<Agent | undefined>
// Internal complexity hidden
const response = await this.api.agent.public[':handle'].$get({
param: { handle: args.handle }
})Best Practices
1. Use Singleton: Always use getInstance() instead of new FictionSDK().
// ✅ Recommended
const sdk = FictionSDK.getInstance({ isDev: true })
// ⚠️ Works but creates singleton anyway
const sdk = new FictionSDK({ isDev: true })2. Don't Override apiBase: Let SDK auto-detect environment.
// ✅ Normal usage
const sdk = FictionSDK.getInstance({ isDev: true })
// ❌ Only for testing
const sdk = FictionSDK.getInstance({
isDev: true,
apiBase: 'http://localhost:9999'
})3. Clear Between Tests: Use clear() for test isolation.
afterEach(() => {
FictionSDK.getInstance({ isDev: true }).clear()
})4. Check Error State: Methods return undefined on error.
const agent = await sdk.getPublicAgent({ handle: 'test' })
if (!agent) {
console.error(sdk.error.value)
}5. Agent-First APIs: Pass agentId, let SDK resolve orgId.
// ✅ Minimal args
await sdk.trackUsage({ agentId, type, quantity })
// ❌ Redundant args
await sdk.trackUsage({ orgId, agentId, type, quantity })Architecture Principles
Minimal Case First: Start with simplest implementation, add complexity only when needed.
No Globals: All state via dependency injection (SDK prop pattern).
Bundle Safety: Server code never reaches client (strict separation).
Validated Environment: Auto-detected apiBase based on environment.
Static Chaining: Preserves Hono type inference for internal operations.
Browser Compatibility
Singleton Scope: globalThis for cross-bundle state sharing.
SSR Detection: typeof window === 'undefined' prevents Node.js singleton.
Storage: localStorage for user data, cookies for auth tokens.
Vue 3.6+: Required for reactive refs and computed properties.
Development
# Watch mode - rebuilds on change
pnpm dev
# Production build (ES modules + types)
pnpm build
# Run tests
pnpm test
# Typecheck
pnpm typecheckBuild required before typecheck - Widget/www import from dist/, not source.
Contributing
This SDK is for Fiction-owned projects only. For internal development:
- Make changes in
packages/sdk/ - Build:
pnpm run sdk:build - Test:
pnpm test packages/sdk/test/ - Update this README if public API changes
License
Proprietary - Fiction Co.
Support
Internal developers: See .ai/spec-sdk.md for complete architecture documentation.
External users: Contact [email protected]
