spaps
Version:
Sweet Potato Authentication & Payment Service CLI - Zero-config local development with built-in admin middleware and permission utilities
812 lines (684 loc) • 22.4 kB
JavaScript
/**
* SPAPS Local Server Documentation HTML Generator
* Generates comprehensive documentation page
*/
function generateDocsHTML(port = 3300, notice) {
return `
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SPAPS Documentation - Local Development Mode</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #333;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.header {
background: white;
border-radius: 12px;
padding: 2rem;
margin-bottom: 2rem;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}
.header h1 {
color: #764ba2;
font-size: 2.5rem;
margin-bottom: 0.5rem;
}
.header .subtitle {
color: #666;
font-size: 1.1rem;
}
.status-badge {
display: inline-block;
background: #48bb78;
color: white;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.9rem;
margin-top: 1rem;
}
.nav {
background: white;
border-radius: 12px;
padding: 1rem;
margin-bottom: 2rem;
box-shadow: 0 5px 15px rgba(0,0,0,0.08);
position: sticky;
top: 1rem;
z-index: 100;
}
.nav ul {
list-style: none;
display: flex;
gap: 2rem;
overflow-x: auto;
padding: 0.5rem;
}
.nav a {
color: #764ba2;
text-decoration: none;
font-weight: 500;
white-space: nowrap;
transition: color 0.3s;
}
.nav a:hover {
color: #667eea;
}
.content {
background: white;
border-radius: 12px;
padding: 2rem;
margin-bottom: 2rem;
box-shadow: 0 5px 15px rgba(0,0,0,0.08);
}
h2 {
color: #764ba2;
margin-top: 2rem;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid #f0f0f0;
}
h3 {
color: #667eea;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
}
pre {
background: #f7f7f7;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 1rem;
overflow-x: auto;
margin: 1rem 0;
position: relative;
}
code {
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', monospace;
font-size: 0.9rem;
background: #f7f7f7;
padding: 0.2rem 0.4rem;
border-radius: 4px;
}
pre code {
background: none;
padding: 0;
}
.endpoint {
background: #f9f9f9;
border-left: 4px solid #667eea;
padding: 1rem;
margin: 1rem 0;
border-radius: 4px;
}
.endpoint strong {
color: #764ba2;
font-family: 'SF Mono', Monaco, monospace;
font-size: 0.95rem;
}
.endpoint .method {
display: inline-block;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-weight: bold;
font-size: 0.85rem;
margin-right: 0.5rem;
}
.endpoint .method.get { background: #48bb78; color: white; }
.endpoint .method.post { background: #4299e1; color: white; }
.endpoint .method.put { background: #ed8936; color: white; }
.endpoint .method.delete { background: #f56565; color: white; }
.tabs {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
border-bottom: 2px solid #f0f0f0;
}
.tab {
padding: 0.5rem 1rem;
cursor: pointer;
border: none;
background: none;
color: #666;
font-size: 1rem;
transition: all 0.3s;
}
.tab.active {
color: #764ba2;
border-bottom: 2px solid #764ba2;
margin-bottom: -2px;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
margin: 2rem 0;
}
.feature-card {
background: #f9f9f9;
padding: 1.5rem;
border-radius: 8px;
transition: transform 0.3s;
}
.feature-card:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.feature-card h3 {
margin-top: 0;
}
.copy-button {
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: #667eea;
color: white;
border: none;
padding: 0.25rem 0.75rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
transition: background 0.3s;
}
.copy-button:hover {
background: #764ba2;
}
.copy-button.copied {
background: #48bb78;
}
.alert {
background: #fef5e7;
border: 1px solid #f9e79f;
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
}
.alert.info {
background: #e8f4fd;
border-color: #bee5eb;
}
.alert.success {
background: #d4edda;
border-color: #c3e6cb;
}
.footer {
text-align: center;
color: white;
padding: 2rem;
}
.footer a {
color: white;
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🍠 SPAPS Documentation</h1>
<p class="subtitle">Sweet Potato Authentication & Payment Service</p>
<span class="status-badge">✅ Local Mode Active - Port ${port}</span>
</div>
${notice ? `<div class="alert">${notice}</div>` : ''}
<nav class="nav">
<ul>
<li><a href="#quickstart">Quick Start</a></li>
<li><a href="#sdk">SDK Setup</a></li>
<li><a href="#authentication">Authentication</a></li>
<li><a href="#payments">Payments</a></li>
<li><a href="#endpoints">API Endpoints</a></li>
<li><a href="#examples">Examples</a></li>
<li><a href="#testing">Testing</a></li>
</ul>
</nav>
<div class="content">
<div class="alert info">
<strong>🚀 Local Development Mode</strong><br>
You're running in local mode. No API keys, database, or external services required!
All responses are mocked for rapid development.
</div>
<section id="quickstart">
<h2>Quick Start</h2>
<h3>1. Install the SDK</h3>
<pre><code>npm install spaps-sdk
# or
yarn add spaps-sdk</code></pre>
<h3>2. Initialize the Client</h3>
<div class="tabs">
<button class="tab active" onclick="showTab(event, 'js-init')">JavaScript</button>
<button class="tab" onclick="showTab(event, 'ts-init')">TypeScript</button>
<button class="tab" onclick="showTab(event, 'react-init')">React</button>
</div>
<div id="js-init" class="tab-content active">
<pre><code>const { SPAPSClient } = require('spaps-sdk');
// Auto-detects local mode - no config needed!
const spaps = new SPAPSClient();
// Login
const { data } = await spaps.login('user@example.com', 'password');
console.log('User:', data.user);
console.log('Token:', data.access_token);</code></pre>
</div>
<div id="ts-init" class="tab-content">
<pre><code>import { SPAPSClient } from 'spaps-sdk';
// TypeScript types included
const spaps = new SPAPSClient({
apiUrl: 'http://localhost:${port}'
});
// Fully typed responses
const { data } = await spaps.login('user@example.com', 'password');
console.log('User ID:', data.user.id);</code></pre>
</div>
<div id="react-init" class="tab-content">
<pre><code>import { SPAPSClient } from 'spaps-sdk';
import { createContext, useContext } from 'react';
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);</code></pre>
</div>
</section>
<section id="sdk">
<h2>SDK Setup & Configuration</h2>
<h3>Installation</h3>
<pre><code>npm install spaps-sdk</code></pre>
<h3>Configuration Options</h3>
<pre><code>const spaps = new SPAPSClient({
// API endpoint (auto-detected from env)
apiUrl: 'http://localhost:${port}',
// API key (not needed for localhost)
apiKey: 'spaps_live_abc123...',
// Request timeout in milliseconds
timeout: 10000,
// Auto-detect local mode (default: true)
autoDetect: true
});</code></pre>
<h3>Environment Variables</h3>
<pre><code># .env
SPAPS_API_URL=http://localhost:${port}
SPAPS_API_KEY=your_api_key_here
# Next.js
NEXT_PUBLIC_SPAPS_API_URL=http://localhost:${port}</code></pre>
<div class="feature-grid">
<div class="feature-card">
<h3>🔌 Auto-Detection</h3>
<p>SDK automatically detects local mode when URL contains localhost or 127.0.0.1</p>
</div>
<div class="feature-card">
<h3>🔑 No API Key</h3>
<p>Local mode doesn't require API keys - perfect for rapid development</p>
</div>
<div class="feature-card">
<h3>📦 TypeScript</h3>
<p>Full TypeScript support with type definitions included</p>
</div>
<div class="feature-card">
<h3>🔄 Auto-Refresh</h3>
<p>Tokens automatically refresh when expired</p>
</div>
<div class="feature-card">
<h3>💳 Mock Stripe</h3>
<p>Checkout & webhooks work instantly - no Stripe account needed!</p>
</div>
<div class="feature-card">
<h3>⚡ Auto Webhooks</h3>
<p>Webhooks fire automatically 2 seconds after payment actions</p>
</div>
</div>
</section>
<section id="authentication">
<h2>Authentication</h2>
<h3>Email/Password Authentication</h3>
<pre><code>// Register new user
const { data } = await spaps.register('user@example.com', 'password');
console.log('New user:', data.user);
// Login existing user
const { data } = await spaps.login('user@example.com', 'password');
console.log('Access token:', data.access_token);
// Get current user
const user = await spaps.getUser();
console.log('Current user:', user.data);
// Logout
await spaps.logout();</code></pre>
<h3>Wallet Authentication</h3>
<pre><code>// Solana wallet
await spaps.walletSignIn(
walletAddress,
signature,
message,
'solana'
);
// Ethereum wallet
await spaps.walletSignIn(
walletAddress,
signature,
message,
'ethereum'
);</code></pre>
<h3>Token Management</h3>
<pre><code>// Check if authenticated
if (spaps.isAuthenticated()) {
console.log('User is logged in');
}
// Get access token
const token = spaps.getAccessToken();
// Set token manually
spaps.setAccessToken(token);
// Refresh token
await spaps.refresh();</code></pre>
</section>
<section id="payments">
<h2>Payment Integration</h2>
<div class="alert" style="background: #e0f2fe; border-color: #0369a1; margin-bottom: 2rem;">
<strong>🎉 New in v0.3.1:</strong> Local Stripe testing with automatic webhook simulation!
<br>• No Stripe account required for local development
<br>• Webhooks fire automatically after payments
<br>• Mock checkout page included
<br>• Test webhook UI at <code>/api/stripe/webhooks/test</code>
</div>
<h3>Stripe Checkout (Works in Local Mode!)</h3>
<pre><code>// Create checkout session - works instantly in local mode!
const session = await spaps.createCheckoutSession(
'price_local_validate', // Use local price IDs
'http://localhost:3000/success', // Success URL
'http://localhost:3000/cancel' // Cancel URL (optional)
);
// In local mode: Returns mock checkout URL
// In production: Returns real Stripe URL
window.location.href = session.data.url;
// Webhook fires automatically after 2 seconds in local mode!</code></pre>
<h3>Local Mode Test Products</h3>
<pre><code>// Pre-configured test products (Buildooor tiers)
const prices = {
'price_local_validate': '$500 - Validate tier',
'price_local_prototype': '$2,500 - Prototype tier',
'price_local_strategy': '$10,000 - Strategy tier',
'price_local_build': '$25,000 - Build tier'
};</code></pre>
<h3>Webhook Testing</h3>
<pre><code>// Your webhook handler (same code for local & production!)
app.post('/webhook', (req, res) => {
const event = req.body;
switch(event.type) {
case 'checkout.session.completed':
// In local: fires 2 seconds after checkout
// In production: fires when payment completes
console.log('Payment successful!');
break;
}
res.json({ received: true });
});
// Test webhooks via UI (local only)
// Visit: http://localhost:${port}/api/stripe/webhooks/test</code></pre>
<h3>Subscription Management</h3>
<pre><code>// Get current subscription
const subscription = await spaps.getSubscription();
console.log('Status:', subscription.data.status);
console.log('Plan:', subscription.data.plan);
// Cancel subscription
await spaps.cancelSubscription();</code></pre>
<h3>Usage Tracking</h3>
<pre><code>// Check balance
const balance = await spaps.getUsageBalance();
console.log('Credits:', balance.data.balance);
// Record usage
await spaps.recordUsage('api-call', 1);
await spaps.recordUsage('image-generation', 10);</code></pre>
</section>
<section id="endpoints">
<h2>API Endpoints</h2>
<h3>Authentication</h3>
<div class="endpoint">
<span class="method post">POST</span>
<strong>/api/auth/register</strong> - Register new user
</div>
<div class="endpoint">
<span class="method post">POST</span>
<strong>/api/auth/login</strong> - Login with email/password
</div>
<div class="endpoint">
<span class="method post">POST</span>
<strong>/api/auth/wallet-sign-in</strong> - Wallet authentication
</div>
<div class="endpoint">
<span class="method post">POST</span>
<strong>/api/auth/refresh</strong> - Refresh access token
</div>
<div class="endpoint">
<span class="method post">POST</span>
<strong>/api/auth/logout</strong> - Logout user
</div>
<div class="endpoint">
<span class="method get">GET</span>
<strong>/api/auth/user</strong> - Get current user
</div>
<h3>Payments</h3>
<div class="endpoint">
<span class="method post">POST</span>
<strong>/api/stripe/create-checkout-session</strong> - Create Stripe checkout
</div>
<div class="endpoint">
<span class="method get">GET</span>
<strong>/api/stripe/subscription</strong> - Get subscription status
</div>
<div class="endpoint">
<span class="method delete">DELETE</span>
<strong>/api/stripe/subscription</strong> - Cancel subscription
</div>
<h3>Usage</h3>
<div class="endpoint">
<span class="method get">GET</span>
<strong>/api/usage/balance</strong> - Get usage balance
</div>
<div class="endpoint">
<span class="method post">POST</span>
<strong>/api/usage/record</strong> - Record usage event
</div>
<h3>Health</h3>
<div class="endpoint">
<span class="method get">GET</span>
<strong>/health</strong> - Health check
</div>
<div class="endpoint">
<span class="method get">GET</span>
<strong>/health/local-mode</strong> - Local mode status
</div>
</section>
<section id="examples">
<h2>Complete Examples</h2>
<h3>React Login Component</h3>
<pre><code>import { useState } from 'react';
import { useSpaps } from './contexts/SpapsContext';
function LoginForm() {
const spaps = useSpaps();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
try {
const { data } = await spaps.login(email, password);
console.log('Logged in:', data.user);
// Redirect to dashboard
} catch (error) {
console.error('Login failed:', error);
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Logging in...' : 'Login'}
</button>
</form>
);
}</code></pre>
<h3>Express.js Middleware</h3>
<pre><code>const express = require('express');
const { SPAPSClient } = require('spaps-sdk');
const app = express();
const spaps = new SPAPSClient();
// 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' });
}
try {
spaps.setAccessToken(token);
const { data } = await spaps.getUser();
req.user = data;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
}
// Protected route
app.get('/api/profile', requireAuth, (req, res) => {
res.json(req.user);
});</code></pre>
<h3>Next.js Server Action</h3>
<pre><code>'use server';
import { SPAPSClient } from 'spaps-sdk';
import { cookies } from 'next/headers';
const spaps = new SPAPSClient();
export async function loginAction(email: string, password: string) {
try {
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 };
} catch (error) {
return { success: false, error: 'Invalid credentials' };
}
}</code></pre>
</section>
<section id="testing">
<h2>Testing</h2>
<h3>Test with cURL</h3>
<pre><code># Health check
curl http://localhost:${port}/health
# Login
curl -X POST http://localhost:${port}/api/auth/login \\
-H "Content-Type: application/json" \\
-d '{"email":"test@example.com","password":"password"}'
# Get user (with token)
curl http://localhost:${port}/api/auth/user \\
-H "Authorization: Bearer YOUR_TOKEN"</code></pre>
<h3>Test with SDK</h3>
<pre><code>// test.js
const { SPAPSClient } = require('spaps-sdk');
async function test() {
const spaps = new SPAPSClient();
// Test login
const { data } = await spaps.login('test@example.com', 'password');
console.log('✅ Login successful:', data.user);
// Test authenticated request
const user = await spaps.getUser();
console.log('✅ Got user:', user.data);
// Test Stripe
const session = await spaps.createCheckoutSession(
'price_123',
'http://localhost:3000/success'
);
console.log('✅ Checkout URL:', session.data.url);
}
test().catch(console.error);</code></pre>
<div class="alert success">
<strong>💡 Pro Tip:</strong> In local mode, all endpoints return successful mock responses.
Use any email/password combination for testing.
</div>
</section>
</div>
<div class="footer">
<p>
<strong>SPAPS v0.2.6</strong> |
<a href="https://github.com/yourusername/sweet-potato">GitHub</a> |
<a href="https://sweetpotato.dev">Website</a> |
<a href="https://discord.gg/sweetpotato">Discord</a>
</p>
<p>Made with 🍠 by the Sweet Potato team</p>
</div>
</div>
<script>
function showTab(event, tabId) {
// Hide all tab contents
const contents = document.querySelectorAll('.tab-content');
contents.forEach(content => content.classList.remove('active'));
// Remove active from all tabs
const tabs = document.querySelectorAll('.tab');
tabs.forEach(tab => tab.classList.remove('active'));
// Show selected tab
document.getElementById(tabId).classList.add('active');
event.target.classList.add('active');
}
// Add copy buttons to code blocks
document.addEventListener('DOMContentLoaded', () => {
const codeBlocks = document.querySelectorAll('pre');
codeBlocks.forEach(block => {
const button = document.createElement('button');
button.className = 'copy-button';
button.textContent = 'Copy';
button.onclick = () => {
const code = block.querySelector('code').textContent;
navigator.clipboard.writeText(code).then(() => {
button.textContent = 'Copied!';
button.classList.add('copied');
setTimeout(() => {
button.textContent = 'Copy';
button.classList.remove('copied');
}, 2000);
});
};
block.appendChild(button);
});
});
</script>
</body>
</html>`;
}
module.exports = { generateDocsHTML };