UNPKG

spaps

Version:

Sweet Potato Authentication & Payment Service CLI - Zero-config local development and project scaffolding

806 lines (680 loc) 22.9 kB
/** * 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 };