@pagelines/sdk
v1.0.915
Published
PageLines SDK for bot management, auth, and billing
Readme
PageLines SDK
Lightweight client library for PageLines bot integration. Built for PageLines-owned properties (app, www, widget).
Installation
npm install @pagelines/sdkQuick Start
import { PageLinesSDK } from '@pagelines/sdk'
// Singleton pattern - shares state across your app
const sdk = PageLinesSDK.getInstance({ isDev: true })
// Get public bot
const bot = await sdk.getPublicBot({ 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.currentBot.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.
Bot-First Architecture: Bots are primary entities. Organization context derived from bot, 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'
})
// Google OAuth popup
await sdk.loginWithGoogle()
// Get current user
const user = await sdk.getCurrentUser()
// Logout
await sdk.logout()Bot Access
// Public bot by handle (no auth)
const bot = await sdk.getPublicBot({ handle: 'andrew' })
// Public bot by email (no auth)
const bot = await sdk.getBotByEmail({ email: '[email protected]' })Usage Tracking
// Unified tracking method
await sdk.trackUsage({
botId: 'agt_456', // Bot 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.currentBot.value // Bot | 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 = PageLinesSDK.getInstance({ isDev: true })
const sdk2 = PageLinesSDK.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.pagelines.com)
window.pagelinesSDK = PageLinesSDK.getInstance({ isDev: true })
// Widget automatically uses singleton
const widget = new PLWidget({ handle: 'andrew' })
console.log(window.pagelinesSDK === widget.sdk) // trueTesting Pattern: Use clear() to reset singleton between tests.
import { afterEach, it, expect } from 'vitest'
afterEach(() => {
const sdk = PageLinesSDK.getInstance({ isDev: true })
sdk.clear() // Clears session + state + destroys singleton
})
it('should test singleton behavior', () => {
const sdk = PageLinesSDK.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 = PageLinesSDK.getInstance({ isDev: true })
// Dev: configured local PageLines port (auto)
// Prod: https://www.pagelines.com (auto)
// ✅ Testing only - custom apiBase
const testSDK = PageLinesSDK.getInstance({
isDev: true,
apiBase: 'http://localhost:9999'
})Usage Patterns
Blog Author Attribution
// Astro component (www.pagelines.com)
import { PageLinesSDK } from '@pagelines/sdk'
const sdk = PageLinesSDK.getInstance({ isDev: import.meta.env.DEV })
const authorBot = await sdk.getBotByEmail({ email: '[email protected]' })
// Use bot data instead of static strings
const authorAvatar = authorBot?.avatar?.url
const authorName = authorBot?.name
const authorBio = authorBot?.summaryWidget Integration
import { PLWidget } from '@pagelines/sdk/widget'
const widget = new PLWidget({
mode: 'modal', // 'inline' | 'popup' | 'modal'
handle: 'andrew', // Or provide bot object directly
context: 'support', // Optional: bot context override
firstMessage: 'Hi!' // Optional: custom greeting
})
// Update without re-mount
widget.update({
bot: newBot,
context: 'sales'
})
// Cleanup
widget.destroy()Profile Page
import { BotChat } from '@pagelines/sdk/bot'
const sdk = PageLinesSDK.getInstance({ isDev: true })
const bot = await sdk.getPublicBot({ handle: 'andrew' })<template>
<BotChat
:sdk="sdk"
:bot="bot"
theme-color="#3b82f6"
/>
</template>Error Handling
All wrapper methods return undefined on error (no exceptions thrown). Check reactive error state for details.
const bot = await sdk.getPublicBot({ handle: 'nonexistent' })
if (!bot) {
console.error(sdk.error.value) // 'Bot not found'
}Build Strategy
SDK Build: ES modules, tree-shakeable, externals (Vue)
- Format: ES modules
- Bundle size: < 256KB
- Build time: 5 seconds
Widget Build: IIFE, single-file CDN bundle
- Format: IIFE (global
window.PLWidget) - Bundle size: < 1MB
- Includes all dependencies
Import Patterns
// ✅ Correct - Use bundled entry points
import { PageLinesSDK } from '@pagelines/sdk'
import { BotChat, BotProvider } from '@pagelines/sdk/bot'
import { PLWidget } from '@pagelines/sdk/widget'
// ❌ Wrong - Deep imports don't work
import { PageLinesSDK } from '@pagelines/sdk/PageLinesSDK'
import BotChat from '@pagelines/sdk/bot/ui/BotChat.vue'Package Exports
{
"exports": {
".": {
"import": "./dist/sdk.js",
"types": "./dist/sdk.d.ts"
},
"./bot": {
"import": "./dist/bot.js",
"types": "./dist/bot.d.ts"
},
"./widget": {
"import": "./dist/widget.js",
"types": "./dist/widget.d.ts"
}
}
}Bot-First Architecture
Bots are primary entities that determine organizational context.
// ✅ Bot contains all context
await sdk.trackUsage({
botId: 'agt_456', // Includes orgId internally
type: 'voice',
quantity: 45
})
// ❌ Don't pass redundant orgId
await sdk.trackUsage({
orgId: 'org_123', // Redundant - derived from botId
botId: 'agt_456',
type: 'voice',
quantity: 45
})Benefits:
- Simpler APIs (fewer parameters)
- Single source of truth (bot determines org)
- Automatic context resolution
- Consistent across all SDK methods
Reactive State Pattern
SDK maintains reactive state that auto-updates components.
const sdk = PageLinesSDK.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.currentBot.value = computed(() => {
const user = sdk.activeUser.value
if (!user?.bots) return undefined
const botId = user.primaryBotId || user.bots[0]?.botId
return user.bots.find(a => a.botId === botId)
})
sdk.currentOrg.value = computed(() => {
const bot = sdk.currentBot.value
if (!bot?.orgId) return undefined
return sdk.activeUser.value?.orgs.find(org =>
org.orgId === bot.orgId
)
})Benefits:
- Components auto-update when user changes
- No manual state management needed
- Consistent with PageLines app patterns
- Bot-first architecture (org derived from bot)
Component Integration
Prop-Based Dependency Injection
SDK components accept sdk prop instead of using global services.
<script setup>
import { PageLinesSDK } from '@pagelines/sdk'
import { BotChat } from '@pagelines/sdk/bot'
const sdk = PageLinesSDK.getInstance({ isDev: true })
const bot = await sdk.getPublicBot({ handle: 'andrew' })
</script>
<template>
<BotChat
:sdk="sdk"
:bot="bot"
theme-color="#3b82f6"
/>
</template>Available Components
BotChat - Main bot interface (chat/voice) BotProvider - Handle → bot resolver wrapper ElBotAbout - Profile information display ElBotChat - Text chat interface ElBotVoice - Voice call interface ElBotSidebar - Navigation sidebar
Security Model
Public-Only Access: getPublicBot() and getBotByEmail() only return bots with visibility: 'public'.
Authentication Required: User operations require a valid opaque session token in sdk.token.value.
Rate Limiting: Widget includes 10 comments/hour per bot, 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<Bot | undefined> instead of Hono RPC types.
// Clean wrapper method
async getPublicBot(args: {
handle: string
}): Promise<Bot | undefined>
// Internal complexity hidden
const response = await this.api.bot.public[':handle'].$get({
param: { handle: args.handle }
})Best Practices
1. Use Singleton: Always use getInstance() instead of new PageLinesSDK().
// ✅ Recommended
const sdk = PageLinesSDK.getInstance({ isDev: true })
// ⚠️ Works but creates singleton anyway
const sdk = new PageLinesSDK({ isDev: true })2. Don't Override apiBase: Let SDK auto-detect environment.
// ✅ Normal usage
const sdk = PageLinesSDK.getInstance({ isDev: true })
// ❌ Only for testing
const sdk = PageLinesSDK.getInstance({
isDev: true,
apiBase: 'http://localhost:9999'
})3. Clear Between Tests: Use clear() for test isolation.
afterEach(() => {
PageLinesSDK.getInstance({ isDev: true }).clear()
})4. Check Error State: Methods return undefined on error.
const bot = await sdk.getPublicBot({ handle: 'test' })
if (!bot) {
console.error(sdk.error.value)
}5. Bot-First APIs: Pass botId, let SDK resolve orgId.
// ✅ Minimal args
await sdk.trackUsage({ botId, type, quantity })
// ❌ Redundant args
await sdk.trackUsage({ orgId, botId, 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 PageLines-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 - PageLines
Support
Internal developers: See plans/architecture/architecture-sdk.md for complete architecture documentation.
External users: Contact [email protected]
