spaps
Version:
Sweet Potato Authentication & Payment Service CLI - Zero-config local development and project scaffolding
806 lines (680 loc) • 22.9 kB
JavaScript
/**
* SPAPS CLI Documentation System
* Comprehensive SDK and API documentation
*/
const chalk = require('chalk');
const prompts = require('prompts');
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('// ES6 Import')}
import { SPAPSClient } from 'spaps-sdk'
${chalk.gray('// CommonJS')}
const { SPAPSClient } = require('spaps-sdk')
${chalk.gray('// Create client (auto-detects local mode)')}
const spaps = new SPAPSClient()
${chalk.gray('// With custom config')}
const spaps = new SPAPSClient({
apiUrl: 'http://localhost:3300',
apiKey: 'your-api-key', ${chalk.gray('// Not needed for localhost')}
timeout: 10000
})
`
},
authentication: {
title: 'Authentication Methods',
content: `
${chalk.green('Email/Password Authentication:')}
${chalk.gray('// Register new user')}
const { data } = await spaps.register(email, password)
console.log('User:', data.user)
console.log('Token:', data.access_token)
${chalk.gray('// Login existing user')}
const { data } = await spaps.login(email, password)
${chalk.gray('// Check authentication status')}
if (spaps.isAuthenticated()) {
const user = await spaps.getUser()
console.log('Current user:', user.data)
}
${chalk.gray('// Logout')}
await spaps.logout()
${chalk.green('Wallet Authentication:')}
${chalk.gray('// Solana wallet')}
await spaps.walletSignIn(
walletAddress,
signature,
message,
'solana'
)
${chalk.gray('// Ethereum wallet')}
await spaps.walletSignIn(
walletAddress,
signature,
message,
'ethereum'
)
${chalk.green('Token Management:')}
${chalk.gray('// Get current token')}
const token = spaps.getAccessToken()
${chalk.gray('// Set token manually')}
spaps.setAccessToken(token)
${chalk.gray('// Refresh token')}
await spaps.refresh()
`
},
payments: {
title: 'Payment Integration',
content: `
${chalk.green('Stripe Checkout:')}
${chalk.gray('// Create checkout session')}
const session = await spaps.createCheckoutSession(
'price_123abc', ${chalk.gray('// Stripe price ID')}
'http://localhost:3000/success', ${chalk.gray('// Success URL')}
'http://localhost:3000/cancel' ${chalk.gray('// Cancel URL (optional)')}
)
${chalk.gray('// Redirect to Stripe')}
window.location.href = session.data.url
${chalk.green('Subscription Management:')}
${chalk.gray('// Get current subscription')}
const subscription = await spaps.getSubscription()
console.log('Status:', subscription.data.status)
console.log('Plan:', subscription.data.plan)
console.log('Renews:', subscription.data.current_period_end)
${chalk.gray('// Cancel subscription')}
await spaps.cancelSubscription()
${chalk.green('Usage Tracking:')}
${chalk.gray('// Check balance')}
const balance = await spaps.getUsageBalance()
console.log('Credits:', balance.data.balance)
${chalk.gray('// Record usage')}
await spaps.recordUsage('api-call', 1)
await spaps.recordUsage('image-generation', 10)
`
},
config: {
title: 'Configuration',
content: `
${chalk.green('Environment Variables:')}
${chalk.gray('# .env or .env.local')}
SPAPS_API_URL=http://localhost:3300
SPAPS_API_KEY=spaps_live_abc123...
${chalk.gray('# Next.js (use NEXT_PUBLIC_ prefix)')}
NEXT_PUBLIC_SPAPS_API_URL=http://localhost:3300
NEXT_PUBLIC_SPAPS_API_KEY=spaps_live_abc123...
${chalk.green('Configuration Options:')}
const spaps = new SPAPSClient({
${chalk.gray('// API endpoint (auto-detected from env)')}
apiUrl: 'http://localhost:3300',
${chalk.gray('// API key (not needed for localhost)')}
apiKey: 'spaps_live_abc123...',
${chalk.gray('// Request timeout in milliseconds')}
timeout: 10000,
${chalk.gray('// Auto-detect local mode (default: true)')}
autoDetect: true
})
${chalk.green('Local Mode Detection:')}
${chalk.gray('// The SDK automatically detects local mode when:')}
- URL contains 'localhost'
- URL contains '127.0.0.1'
- No API URL is provided
${chalk.gray('// Check if in local mode')}
if (spaps.isLocalMode()) {
console.log('Running in local mode - no API key needed!')
}
`
},
react: {
title: 'React Integration',
content: `
${chalk.green('React Context Setup:')}
${chalk.gray('// contexts/SpapsContext.tsx')}
import { createContext, useContext } from 'react'
import { SPAPSClient } from 'spaps-sdk'
const spaps = new SPAPSClient()
const SpapsContext = createContext(spaps)
export function SpapsProvider({ children }) {
return (
<SpapsContext.Provider value={spaps}>
{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 { SPAPSClient } from 'spaps-sdk'
import { cookies } from 'next/headers'
const spaps = new SPAPSClient({
apiUrl: process.env.SPAPS_API_URL,
apiKey: process.env.SPAPS_API_KEY
})
export async function loginAction(email: string, password: string) {
const { data } = await spaps.login(email, password)
// Store token in cookie
cookies().set('spaps_token', data.access_token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7 // 1 week
})
return { success: true, user: data.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 { SPAPSClient } = require('spaps-sdk')
const app = express()
const spaps = new SPAPSClient({
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 = spaps
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 { data } = await req.spaps.getUser()
req.user = data
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 { data } = await req.spaps.login(email, password)
res.json(data)
} 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:3300'
})
${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:3300'
})
})
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', () => ({
SPAPSClient: jest.fn().mockImplementation(() => ({
login: jest.fn().mockResolvedValue({
data: {
user: { id: '123', email: 'test@example.com' },
access_token: 'mock-token'
}
}),
isAuthenticated: jest.fn().mockReturnValue(true),
getUser: jest.fn().mockResolvedValue({
data: { id: '123', email: 'test@example.com' }
})
}))
}))
`
},
api_reference: {
title: 'API Reference',
content: `
${chalk.green('Authentication Methods:')}
login(email: string, password: string): Promise<{data: AuthResponse}>
register(email: string, password: string): Promise<{data: AuthResponse}>
walletSignIn(address: string, signature: string, message: string, chain: string): Promise<{data: AuthResponse}>
refresh(refreshToken?: string): Promise<{data: AuthResponse}>
logout(): Promise<void>
getUser(): Promise<{data: User}>
${chalk.green('Payment Methods:')}
createCheckoutSession(priceId: string, successUrl: string, cancelUrl?: string): Promise<{data: CheckoutSession}>
getSubscription(): Promise<{data: Subscription}>
cancelSubscription(): Promise<void>
${chalk.green('Usage Methods:')}
getUsageBalance(): Promise<{data: UsageBalance}>
recordUsage(feature: string, amount: number): Promise<void>
${chalk.green('Utility Methods:')}
isAuthenticated(): boolean
getAccessToken(): string | undefined
setAccessToken(token: string): void
isLocalMode(): boolean
health(): Promise<{data: any}>
${chalk.green('Properties:')}
client: AxiosInstance ${chalk.gray('// Direct access to Axios client')}
${chalk.green('Response Types:')}
${chalk.gray('// All methods return data wrapped in { data: T }')}
${chalk.gray('// This matches Axios response structure')}
${chalk.gray('// Example:')}
const response = await spaps.login(email, password)
// response.data contains AuthResponse
// response.status, response.headers also available
`
}
};
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/create-checkout-session ${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:3300):')}
• 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 showQuickReference() {
console.log(chalk.yellow('\n🍠 SPAPS SDK Quick Reference\n'));
console.log(chalk.green('Installation:'));
console.log(' npm install spaps-sdk');
console.log();
console.log(chalk.green('Basic Setup:'));
console.log(chalk.gray(` import { SPAPSClient } from 'spaps-sdk'`));
console.log(chalk.gray(` const spaps = new SPAPSClient()`));
console.log();
console.log(chalk.green('Common Methods:'));
console.log(' await spaps.login(email, password)');
console.log(' await spaps.register(email, password)');
console.log(' await spaps.getUser()');
console.log(' await spaps.createCheckoutSession(priceId, successUrl)');
console.log(' await spaps.getSubscription()');
console.log(' await spaps.getUsageBalance()');
console.log();
console.log(chalk.green('Helper Methods:'));
console.log(' spaps.isAuthenticated() ' + chalk.gray('// Check auth status'));
console.log(' spaps.isLocalMode() ' + chalk.gray('// Check if local mode'));
console.log(' spaps.getAccessToken() ' + chalk.gray('// Get current token'));
console.log();
console.log(chalk.blue('📚 Full docs: npx spaps docs --interactive'));
console.log();
}
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
};