npm package: @fiction/sdk
Description: SDK for Fiction app authentication and user management
Author: Fiction.com
Latest version: 1.0.125

# Fiction SDK

Lightweight client library for Fiction digital agent integration. Built for Fiction-owned properties (app, www, widget).

## Installation

```bash
npm install @fiction/sdk
```

## Quick Start

```typescript
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: 'user@example.com' })
await sdk.loginWithCode({ email: 'user@example.com', 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

```typescript
// Request email verification code
await sdk.requestAuthCode({ email: 'user@example.com' })

// Login with code
await sdk.loginWithCode({
  email: 'user@example.com',
  code: '123456'
})

// Get current user
const user = await sdk.getCurrentUser()

// Logout
await sdk.logout()
```

### Digital Agent Access

```typescript
// 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: 'andrew@fiction.com' })
```

### Usage Tracking

```typescript
// 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

```typescript
// 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 | undefined
```

## Singleton Pattern

**Why Singleton**: Prevents duplicate API requests and maintains shared reactive state across your application.

```typescript
// 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).

```typescript
// 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) // true
```

**Testing Pattern**: Use `clear()` to reset singleton between tests.

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

```typescript
// ✅ 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

```typescript
// 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: 'andrew@fiction.com' })

// Use digital agent data instead of static strings
const authorAvatar = authorAgent?.avatar?.url
const authorName = authorAgent?.name
const authorBio = authorAgent?.summary
```

### Widget Integration

```typescript
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

```typescript
import { AgentChat } from '@fiction/sdk/agent'

const sdk = FictionSDK.getInstance({ isDev: true })
const agent = await sdk.getPublicAgent({ handle: 'andrew' })
```

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

```typescript
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

```typescript
// ✅ 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

```json
{
  "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.

```typescript
// ✅ 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.

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

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

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

```typescript
// 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()`.

```typescript
// ✅ 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.

```typescript
// ✅ 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.

```typescript
afterEach(() => {
  FictionSDK.getInstance({ isDev: true }).clear()
})
```

**4. Check Error State**: Methods return `undefined` on error.

```typescript
const agent = await sdk.getPublicAgent({ handle: 'test' })
if (!agent) {
  console.error(sdk.error.value)
}
```

**5. Agent-First APIs**: Pass `agentId`, let SDK resolve `orgId`.

```typescript
// ✅ 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

```bash
# Watch mode - rebuilds on change
pnpm dev

# Production build (ES modules + types)
pnpm build

# Run tests
pnpm test

# Typecheck
pnpm typecheck
```

**Build required before typecheck** - Widget/www import from `dist/`, not source.

## Contributing

This SDK is for Fiction-owned projects only. For internal development:

1. Make changes in `packages/sdk/`
2. Build: `pnpm run sdk:build`
3. Test: `pnpm test packages/sdk/test/`
4. 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 support@fiction.com