spaps
Version:
Sweet Potato Authentication & Payment Service CLI - Docker Compose orchestrator for local Python/FastAPI SPAPS server with built-in admin middleware
726 lines (613 loc) • 21 kB
JavaScript
/**
* SPAPS CLI Documentation System
* Comprehensive SDK and API documentation
*/
const chalk = require('chalk');
const prompts = require('prompts');
const { showQuickReference } = require('./docs-quick');
const SDK_DOCS = {
quickstart: {
title: 'SDK Quick Start',
content: `
${chalk.green('Installation:')}
npm install spaps-sdk
# or
yarn add spaps-sdk
${chalk.green('Basic Usage:')}
${chalk.gray('// ES Module')}
import { SweetPotatoSDK } from 'spaps-sdk'
${chalk.gray('// CommonJS')}
const { SweetPotatoSDK } = require('spaps-sdk')
${chalk.gray('// Create client for local or provisioned SPAPS')}
const sdk = new SweetPotatoSDK({
apiUrl: process.env.SPAPS_API_URL || 'http://localhost:3301',
apiKey: process.env.SPAPS_API_KEY, ${chalk.gray('// Required unless /health/local-mode says otherwise')}
})
${chalk.gray('// Sign in with email/password')}
const auth = await sdk.auth.signInWithPassword({ email: 'user@example.com', password: 'password' })
console.log('User:', auth.user)
`
},
authentication: {
title: 'Authentication Methods',
content: `
${chalk.green('Email/Password Authentication:')}
${chalk.gray('// Register new user')}
const registered = await sdk.auth.register({ email, password })
console.log('User:', registered.user)
console.log('Token:', registered.access_token)
${chalk.gray('// Login existing user')}
const auth = await sdk.auth.signInWithPassword({ email, password })
${chalk.gray('// Check authentication status')}
if (sdk.auth.isAuthenticated()) {
const user = await sdk.auth.getCurrentUser()
console.log('Current user:', user)
}
${chalk.gray('// Logout')}
await sdk.auth.logout()
${chalk.green('Wallet Authentication:')}
${chalk.gray('// One-call helper: authenticateWallet')}
const resp = await sdk.auth.authenticateWallet(walletAddress, signMessage, 'ethereum')
console.log('User:', resp.user)
${chalk.green('Token Management:')}
${chalk.gray('// Access token is managed internally; you can also set it manually')}
sdk.setAccessToken('jwt-token')
`
},
payments: {
title: 'Payment Integration',
content: `
${chalk.green('Stripe Checkout:')}
${chalk.gray('// Create checkout session')}
const session = await sdk.payments.createPaymentCheckout({
price_id: 'price_123abc',
success_url: 'http://localhost:3000/success',
cancel_url: 'http://localhost:3000/cancel'
})
${chalk.gray('// Redirect to Stripe')}
window.location.href = session.url
${chalk.green('Subscription Management:')}
${chalk.gray('// Get current subscription')}
// Example subscription helpers would go here if enabled
${chalk.gray('// Cancel subscription')}
await spaps.cancelSubscription(subscriptionId)
${chalk.green('Usage Tracking:')}
${chalk.gray('// Check balance')}
// Usage APIs depend on your server config
`
},
config: {
title: 'Configuration',
content: `
${chalk.green('Environment Variables:')}
${chalk.gray('# .env or .env.local')}
SPAPS_API_URL=http://localhost:3301
SPAPS_API_KEY=spaps_live_abc123...
${chalk.gray('# Next.js (use NEXT_PUBLIC_ prefix)')}
NEXT_PUBLIC_SPAPS_API_URL=http://localhost:3301
NEXT_PUBLIC_SPAPS_API_KEY=spaps_live_abc123...
${chalk.green('Configuration Options:')}
const sdk = new SweetPotatoSDK({
apiUrl: process.env.SPAPS_API_URL || 'http://localhost:3301',
apiKey: process.env.SPAPS_API_KEY, ${chalk.gray('// Omit only when /health/local-mode reports local_mode_active: true')}
})
${chalk.green('Local Mode Detection:')}
${chalk.gray('// Localhost URLs still need the running server to advertise local mode:')}
- URL contains 'localhost'
- URL contains '127.0.0.1'
- No API URL is provided
${chalk.gray('// Verify against the server before assuming no key is required')}
if (sdk.isLocalMode) {
console.log('Check /health/local-mode before skipping API keys.')
}
`
},
react: {
title: 'React Integration',
content: `
${chalk.green('React Context Setup:')}
${chalk.gray('// contexts/SpapsContext.tsx')}
import { createContext, useContext } from 'react'
import { SweetPotatoSDK } from 'spaps-sdk'
const sdk = new SweetPotatoSDK({ apiUrl: process.env.NEXT_PUBLIC_SPAPS_API_URL || 'http://localhost:3301' })
const SpapsContext = createContext(sdk)
export function SpapsProvider({ children }) {
return (
<SpapsContext.Provider value={sdk}>
{children}
</SpapsContext.Provider>
)
}
export const useSpaps = () => useContext(SpapsContext)
${chalk.green('React Hook Example:')}
${chalk.gray('// hooks/useAuth.ts')}
import { useState, useEffect } from 'react'
import { useSpaps } from '../contexts/SpapsContext'
export function useAuth() {
const spaps = useSpaps()
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
if (spaps.isAuthenticated()) {
spaps.getUser()
.then(res => setUser(res.data))
.finally(() => setLoading(false))
} else {
setLoading(false)
}
}, [])
return { user, loading, isAuthenticated: !!user }
}
${chalk.green('Component Example:')}
${chalk.gray('// components/LoginForm.tsx')}
function LoginForm() {
const spaps = useSpaps()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleSubmit = async (e) => {
e.preventDefault()
try {
await spaps.login(email, password)
// Redirect or update state
} catch (error) {
console.error('Login failed:', error)
}
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button type="submit">Login</button>
</form>
)
}
`
},
nextjs: {
title: 'Next.js Integration',
content: `
${chalk.green('App Router Setup:')}
${chalk.gray('// app/providers.tsx')}
'use client'
import { SPAPSClient } from 'spaps-sdk'
import { createContext, useContext } from 'react'
const SpapsContext = createContext<SPAPSClient | null>(null)
export function Providers({ children }) {
const spaps = new SPAPSClient({
apiUrl: process.env.NEXT_PUBLIC_SPAPS_API_URL
})
return (
<SpapsContext.Provider value={spaps}>
{children}
</SpapsContext.Provider>
)
}
export const useSpaps = () => {
const context = useContext(SpapsContext)
if (!context) throw new Error('Missing SpapsProvider')
return context
}
${chalk.green('Server Actions:')}
${chalk.gray('// app/actions/auth.ts')}
'use server'
import { SweetPotatoSDK } from 'spaps-sdk'
import { cookies } from 'next/headers'
const sdk = new SweetPotatoSDK({ apiUrl: process.env.SPAPS_API_URL, apiKey: process.env.SPAPS_API_KEY })
export async function loginAction(email: string, password: string) {
const auth = await sdk.auth.signInWithPassword({ email, password })
// Store token in cookie
cookies().set('spaps_token', auth.access_token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7 // 1 week
})
return { success: true, user: auth.user }
}
${chalk.green('Middleware Protection:')}
${chalk.gray('// middleware.ts')}
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('spaps_token')
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: '/dashboard/:path*'
}
`
},
nodejs: {
title: 'Node.js/Express Integration',
content: `
${chalk.green('Express Middleware:')}
${chalk.gray('// server.js')}
const express = require('express')
const { SweetPotatoSDK } = require('spaps-sdk')
const app = express()
const sdk = new SweetPotatoSDK({ apiUrl: process.env.SPAPS_API_URL, apiKey: process.env.SPAPS_API_KEY })
${chalk.gray('// Add SPAPS to request')}
app.use((req, res, next) => {
req.spaps = sdk
next()
})
${chalk.gray('// Auth middleware')}
async function requireAuth(req, res, next) {
const token = req.headers.authorization?.split(' ')[1]
if (!token) {
return res.status(401).json({ error: 'No token provided' })
}
try {
req.spaps.setAccessToken(token)
const user = await req.spaps.auth.getCurrentUser()
req.user = user
next()
} catch (error) {
res.status(401).json({ error: 'Invalid token' })
}
}
${chalk.green('Route Examples:')}
${chalk.gray('// Login endpoint')}
app.post('/api/login', async (req, res) => {
const { email, password } = req.body
try {
const auth = await req.spaps.auth.signInWithPassword({ email, password })
res.json(auth)
} catch (error) {
res.status(401).json({ error: 'Invalid credentials' })
}
})
${chalk.gray('// Protected route')}
app.get('/api/profile', requireAuth, (req, res) => {
res.json(req.user)
})
${chalk.gray('// Stripe webhook')}
app.post('/api/webhooks/stripe', async (req, res) => {
// Verify webhook signature
// Update user subscription status
res.json({ received: true })
})
`
},
errors: {
title: 'Error Handling',
content: `
${chalk.green('Error Types:')}
try {
await spaps.login(email, password)
} catch (error) {
if (error.response) {
${chalk.gray('// Server responded with error')}
console.log('Status:', error.response.status)
console.log('Data:', error.response.data)
switch (error.response.status) {
case 401:
console.error('Invalid credentials')
break
case 429:
console.error('Rate limited - try again later')
break
case 500:
console.error('Server error')
break
}
} else if (error.request) {
${chalk.gray('// Request made but no response')}
console.error('Network error - server unreachable')
} else {
${chalk.gray('// Error in request setup')}
console.error('Request error:', error.message)
}
}
${chalk.green('Custom Error Handling:')}
${chalk.gray('// Global error handler')}
spaps.client.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
// Token expired - redirect to login
window.location.href = '/login'
}
return Promise.reject(error)
}
)
${chalk.green('Retry Logic:')}
${chalk.gray('// Retry failed requests')}
async function retryRequest(fn, retries = 3) {
try {
return await fn()
} catch (error) {
if (retries > 0 && error.response?.status >= 500) {
await new Promise(r => setTimeout(r, 1000))
return retryRequest(fn, retries - 1)
}
throw error
}
}
${chalk.gray('// Usage')}
const user = await retryRequest(() => spaps.getUser())
`
},
typescript: {
title: 'TypeScript Support',
content: `
${chalk.green('Type Definitions:')}
import {
SPAPSClient,
SPAPSConfig,
AuthResponse,
User,
CheckoutSession,
Subscription,
UsageBalance
} from 'spaps-sdk'
${chalk.green('Interface Examples:')}
${chalk.gray('// User type')}
interface User {
id: string
email?: string
wallet_address?: string
chain_type?: string
role: string
created_at?: string
}
${chalk.gray('// Auth response')}
interface AuthResponse {
access_token: string
refresh_token: string
user: User
}
${chalk.gray('// Config options')}
interface SPAPSConfig {
apiUrl?: string
apiKey?: string
autoDetect?: boolean
timeout?: number
}
${chalk.green('Type-Safe Usage:')}
${chalk.gray('// Typed client')}
const spaps: SPAPSClient = new SPAPSClient({
apiUrl: 'http://localhost:3301'
})
${chalk.gray('// Typed responses')}
const login = async (email: string, password: string): Promise<User> => {
const { data }: { data: AuthResponse } = await spaps.login(email, password)
return data.user
}
${chalk.gray('// Generic wrapper')}
async function apiCall<T>(
fn: () => Promise<{ data: T }>
): Promise<T> {
try {
const { data } = await fn()
return data
} catch (error) {
console.error('API call failed:', error)
throw error
}
}
${chalk.gray('// Usage')}
const user = await apiCall<User>(() => spaps.getUser())
`
},
testing: {
title: 'Testing',
content: `
${chalk.green('Unit Testing with Jest:')}
${chalk.gray('// __tests__/auth.test.js')}
import { SPAPSClient } from 'spaps-sdk'
describe('Authentication', () => {
let spaps
beforeEach(() => {
spaps = new SPAPSClient({
apiUrl: 'http://localhost:3301'
})
})
test('login returns user and token', async () => {
const { data } = await spaps.login('test@example.com', 'password')
expect(data.user).toBeDefined()
expect(data.user.email).toBe('test@example.com')
expect(data.access_token).toBeDefined()
})
test('sets auth token after login', async () => {
await spaps.login('test@example.com', 'password')
expect(spaps.isAuthenticated()).toBe(true)
expect(spaps.getAccessToken()).toBeDefined()
})
})
${chalk.green('E2E Testing with Cypress:')}
${chalk.gray('// cypress/e2e/auth.cy.js')}
describe('Auth Flow', () => {
beforeEach(() => {
cy.visit('http://localhost:3000')
})
it('allows user to login', () => {
cy.get('[data-cy=email]').type('test@example.com')
cy.get('[data-cy=password]').type('password')
cy.get('[data-cy=submit]').click()
cy.url().should('include', '/dashboard')
cy.contains('Welcome back')
})
})
${chalk.green('Mocking for Tests:')}
${chalk.gray('// Mock SPAPS client')}
jest.mock('spaps-sdk', () => ({
SweetPotatoSDK: jest.fn().mockImplementation(() => ({
auth: {
signInWithPassword: jest.fn().mockResolvedValue({
user: { id: '123', email: 'test@example.com' },
access_token: 'mock-token',
refresh_token: 'mock-refresh'
}),
isAuthenticated: jest.fn().mockReturnValue(true),
getCurrentUser: jest.fn().mockResolvedValue({ id: '123', email: 'test@example.com' })
},
setAccessToken: jest.fn()
}))
}))
`
},
api_reference: {
title: 'API Reference',
content: `
${chalk.green('Authentication Methods:')}
sdk.auth.signInWithPassword({ email, password }): Promise<AuthResponse>
sdk.auth.register({ email, password, username? }): Promise<AuthResponse>
sdk.auth.authenticateWallet(address, signFn, chain?): Promise<AuthResponse>
sdk.auth.refreshToken(refreshToken): Promise<AuthResponse>
sdk.auth.logout(): Promise<void>
sdk.auth.getCurrentUser(): Promise<User>
${chalk.green('Payment Methods:')}
sdk.payments.createCheckoutSession({ price_id, success_url, cancel_url }): Promise<CheckoutSession>
sdk.payments.getCheckoutSession(id): Promise<CheckoutSession>
sdk.payments.listProducts({ category?, active?, limit? }): Promise<ProductsListResponse>
${chalk.green('Usage Methods:')}
See server docs if enabled
${chalk.green('Utility Methods:')}
sdk.setAccessToken(token: string): void
sdk.clearAccessToken(): void
${chalk.green('Response Types:')}
${chalk.gray('// Methods generally return typed objects directly')}
const auth = await sdk.auth.signInWithPassword({ email, password })
console.log(auth.access_token)
`
}
};
const API_ENDPOINTS = {
title: 'API Endpoints',
content: `
${chalk.green('Authentication Endpoints:')}
POST /api/auth/register ${chalk.gray('Register new user')}
POST /api/auth/login ${chalk.gray('Login with email/password')}
POST /api/auth/wallet-sign-in ${chalk.gray('Login with wallet')}
POST /api/auth/refresh ${chalk.gray('Refresh access token')}
POST /api/auth/logout ${chalk.gray('Logout user')}
GET /api/auth/user ${chalk.gray('Get current user')}
${chalk.green('Stripe Endpoints:')}
POST /api/stripe/checkout-sessions ${chalk.gray('Create Stripe checkout')}
GET /api/stripe/subscription ${chalk.gray('Get subscription status')}
DELETE /api/stripe/subscription ${chalk.gray('Cancel subscription')}
POST /api/stripe/webhook ${chalk.gray('Stripe webhook handler')}
${chalk.green('Usage Endpoints:')}
GET /api/usage/balance ${chalk.gray('Get usage balance')}
POST /api/usage/record ${chalk.gray('Record usage event')}
GET /api/usage/history ${chalk.gray('Get usage history')}
${chalk.green('Health Endpoints:')}
GET /health ${chalk.gray('Health check')}
GET /health/local-mode ${chalk.gray('Local mode status')}
${chalk.green('Local Mode Features:')}
${chalk.gray('In local mode (http://localhost:3301):')}
• No API key required
• Auto-authentication enabled
• CORS disabled for all origins
• Mock responses for all endpoints
• Test users available: user, admin, premium
`
};
async function showDocsMenu() {
const choices = [
{ title: '🚀 Quick Start', value: 'quickstart' },
{ title: '🔐 Authentication', value: 'authentication' },
{ title: '💳 Payments', value: 'payments' },
{ title: '⚙️ Configuration', value: 'config' },
{ title: '⚛️ React Integration', value: 'react' },
{ title: '▲ Next.js Integration', value: 'nextjs' },
{ title: '🟢 Node.js/Express', value: 'nodejs' },
{ title: '❌ Error Handling', value: 'errors' },
{ title: '📘 TypeScript', value: 'typescript' },
{ title: '🧪 Testing', value: 'testing' },
{ title: '📖 API Reference', value: 'api_reference' },
{ title: '🌐 API Endpoints', value: 'endpoints' },
{ title: chalk.gray('← Back'), value: '__back__' },
{ title: chalk.gray('✕ Exit'), value: '__exit__' }
];
const response = await prompts({
type: 'select',
name: 'section',
message: 'Select documentation section:',
choices: choices
});
return response.section;
}
async function showInteractiveDocs() {
console.log(chalk.yellow('\n🍠 SPAPS SDK Documentation\n'));
while (true) {
const section = await showDocsMenu();
if (!section || section === '__exit__') {
console.log(chalk.gray('\nGoodbye! 👋\n'));
process.exit(0);
}
if (section === '__back__') {
return; // Go back to main help
}
if (section === 'endpoints') {
console.log(chalk.yellow(`\n${API_ENDPOINTS.title}`));
console.log(API_ENDPOINTS.content);
} else if (SDK_DOCS[section]) {
const doc = SDK_DOCS[section];
console.log(chalk.yellow(`\n${doc.title}`));
console.log(doc.content);
}
// Wait for user to read
const cont = await prompts({
type: 'confirm',
name: 'continue',
message: 'Continue browsing docs?',
initial: true
});
if (!cont.continue) {
console.log(chalk.gray('\nGoodbye! 👋\n'));
process.exit(0);
}
}
}
function searchDocs(query) {
const results = [];
const searchTerm = query.toLowerCase();
// Search through all documentation
Object.entries(SDK_DOCS).forEach(([key, doc]) => {
const content = doc.content.toLowerCase();
const title = doc.title.toLowerCase();
if (title.includes(searchTerm) || content.includes(searchTerm)) {
// Count occurrences
const titleMatches = (title.match(new RegExp(searchTerm, 'g')) || []).length;
const contentMatches = (content.match(new RegExp(searchTerm, 'g')) || []).length;
results.push({
section: key,
title: doc.title,
score: titleMatches * 10 + contentMatches,
preview: extractPreview(doc.content, searchTerm)
});
}
});
// Sort by relevance
results.sort((a, b) => b.score - a.score);
return results.slice(0, 5); // Top 5 results
}
function extractPreview(content, searchTerm) {
const lines = content.split('\n');
for (const line of lines) {
if (line.toLowerCase().includes(searchTerm)) {
return line.trim().substring(0, 80) + '...';
}
}
return lines[0].trim().substring(0, 80) + '...';
}
module.exports = {
showInteractiveDocs,
showQuickReference,
searchDocs,
SDK_DOCS,
API_ENDPOINTS
};