spaps
Version:
Sweet Potato Authentication & Payment Service CLI - Zero-config local development and project scaffolding
404 lines (357 loc) ⢠12.5 kB
JavaScript
#!/usr/bin/env node
/**
* SPAPS Local Development Server
* Minimal, zero-config server for local development
*/
const express = require('express');
const cors = require('cors');
const chalk = require('chalk');
const { generateDocsHTML } = require('./docs-html');
const StripeLocalManager = require('./stripe-local');
class LocalServer {
constructor(options = {}) {
this.port = options.port || process.env.PORT || 3300;
this.json = options.json || false;
this.app = express();
this.stripeManager = null;
this.setupMiddleware();
this.setupRoutes();
this.setupStripeRoutes();
this.setupCatchAll();
}
setupMiddleware() {
// CORS - allow everything in local mode
this.app.use(cors({
origin: true,
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key', 'X-Test-User'],
}));
// Body parsing
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: true }));
// Local mode indicator
this.app.use((req, res, next) => {
res.setHeader('X-SPAPS-Mode', 'local-development');
// Auto-auth in local mode
if (!req.headers.authorization && !req.headers['x-api-key']) {
req.headers['x-api-key'] = 'local-dev-key';
req.user = {
id: 'local-user-123',
email: 'dev@localhost',
role: req.query._user || req.headers['x-test-user'] || 'user'
};
}
// Log requests (unless in JSON mode)
if (!this.json) {
console.log(chalk.dim(`${req.method} ${req.path}`));
}
next();
});
}
setupRoutes() {
// Health check
this.app.get('/health', (req, res) => {
res.json({
status: 'healthy',
mode: 'local-development',
version: '0.2.0',
timestamp: new Date().toISOString()
});
});
// Local mode status
this.app.get('/health/local-mode', (req, res) => {
res.json({
enabled: true,
environment: 'local-development',
features: {
autoAuth: true,
corsEnabled: true,
testUsers: ['user', 'admin', 'premium'],
apiKeyRequired: false
}
});
});
// Mock authentication endpoints
this.app.post('/api/auth/login', (req, res) => {
const { email, password } = req.body;
res.json({
access_token: 'local-jwt-token-' + Date.now(),
refresh_token: 'local-refresh-token-' + Date.now(),
user: {
id: 'local-user-123',
email: email || 'dev@localhost',
role: 'user'
}
});
});
this.app.post('/api/auth/register', (req, res) => {
const { email, password } = req.body;
res.json({
access_token: 'local-jwt-token-' + Date.now(),
refresh_token: 'local-refresh-token-' + Date.now(),
user: {
id: 'local-user-' + Date.now(),
email: email || 'dev@localhost',
role: 'user'
}
});
});
this.app.post('/api/auth/wallet-sign-in', (req, res) => {
const { wallet_address, chain_type } = req.body;
res.json({
access_token: 'local-jwt-token-' + Date.now(),
refresh_token: 'local-refresh-token-' + Date.now(),
user: {
id: 'local-wallet-user-123',
wallet_address,
chain_type,
role: 'user'
}
});
});
this.app.post('/api/auth/refresh', (req, res) => {
res.json({
access_token: 'local-jwt-token-refreshed-' + Date.now(),
refresh_token: 'local-refresh-token-refreshed-' + Date.now()
});
});
this.app.post('/api/auth/logout', (req, res) => {
res.json({ success: true, message: 'Logged out successfully' });
});
this.app.get('/api/auth/user', (req, res) => {
res.json({
id: req.user?.id || 'local-user-123',
email: req.user?.email || 'dev@localhost',
role: req.user?.role || 'user',
created_at: new Date().toISOString()
});
});
// Mock Stripe endpoints
this.app.post('/api/stripe/create-checkout-session', (req, res) => {
res.json({
sessionId: 'cs_test_local_' + Date.now(),
url: 'https://checkout.stripe.com/pay/cs_test_local'
});
});
this.app.get('/api/stripe/subscription', (req, res) => {
res.json({
id: 'sub_local_123',
status: 'active',
plan: 'premium',
current_period_end: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString()
});
});
// Mock usage endpoints
this.app.get('/api/usage/balance', (req, res) => {
res.json({
balance: 1000,
currency: 'credits',
updated_at: new Date().toISOString()
});
});
// Documentation endpoint
this.app.get('/docs', (req, res) => {
res.send(generateDocsHTML(this.port));
});
}
setupStripeRoutes() {
// Mock Stripe checkout session
this.app.post('/api/stripe/create-checkout-session', async (req, res) => {
const { price_id, success_url, cancel_url } = req.body;
const sessionId = 'cs_local_' + Date.now();
res.json({
sessionId,
url: `http://localhost:${this.port}/checkout/${sessionId}?success=${encodeURIComponent(success_url)}&cancel=${encodeURIComponent(cancel_url)}`
});
// Simulate webhook after delay
setTimeout(async () => {
try {
await this.simulateCheckoutWebhook(sessionId, price_id);
if (!this.json) {
console.log(chalk.blue(`ā” Webhook simulated: checkout.session.completed`));
}
} catch (error) {
console.error(chalk.red('Webhook simulation failed:'), error);
}
}, 2000);
});
// Mock checkout page
this.app.get('/checkout/:sessionId', (req, res) => {
const { sessionId } = req.params;
const { success, cancel } = req.query;
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>SPAPS Local - Mock Checkout</title>
<style>
body { font-family: system-ui; max-width: 400px; margin: 100px auto; padding: 2rem; }
button { width: 100%; padding: 1rem; margin: 0.5rem 0; border: none; border-radius: 8px; cursor: pointer; font-size: 16px; }
.pay { background: #635bff; color: white; }
.pay:hover { background: #4b41e0; }
.cancel { background: #f5f5f5; }
.cancel:hover { background: #e5e5e5; }
</style>
</head>
<body>
<h1>š Mock Checkout</h1>
<p>Session: ${sessionId}</p>
<p>This is a mock checkout page for local development.</p>
<button class="pay" onclick="window.location='${success}'">
š³ Complete Payment
</button>
<button class="cancel" onclick="window.location='${cancel}'">
Cancel
</button>
<p style="margin-top: 2rem; color: #666; font-size: 14px;">
In production, this would be a real Stripe Checkout page.
</p>
</body>
</html>
`);
});
// Mock webhook endpoint
this.app.post('/api/stripe/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
// In local mode, accept all webhooks
const event = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
if (!this.json) {
console.log(chalk.blue(`ā” Webhook received: ${event.type}`));
}
// Store for testing
this.lastWebhookEvent = event;
res.json({ received: true });
});
// Webhook testing UI
this.app.get('/api/stripe/webhooks/test', (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>SPAPS - Stripe Webhook Tester</title>
<style>
body { font-family: system-ui; max-width: 800px; margin: 0 auto; padding: 2rem; }
button { background: #635bff; color: white; border: none; padding: 0.75rem 1.5rem; border-radius: 6px; cursor: pointer; margin: 0.25rem; }
button:hover { background: #4b41e0; }
.event { background: #f9f9f9; padding: 1rem; margin: 1rem 0; border-radius: 8px; border-left: 4px solid #635bff; }
</style>
</head>
<body>
<h1>š Stripe Webhook Tester</h1>
<h2>Simulate Events</h2>
<div>
<button onclick="simulate('checkout.session.completed')">Checkout Completed</button>
<button onclick="simulate('payment_intent.succeeded')">Payment Success</button>
<button onclick="simulate('customer.subscription.created')">Subscription Created</button>
</div>
<h2>Last Event</h2>
<div id="lastEvent">No events yet</div>
<script>
async function simulate(type) {
const response = await fetch('/api/stripe/webhooks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: 'evt_local_' + Date.now(),
type: type,
data: { object: { id: type.split('.')[0] + '_' + Date.now() } }
})
});
if (response.ok) {
document.getElementById('lastEvent').innerHTML =
'<div class="event">ā
' + type + ' - ' + new Date().toLocaleTimeString() + '</div>';
}
}
</script>
</body>
</html>
`);
});
}
async simulateCheckoutWebhook(sessionId, priceId) {
const event = {
id: 'evt_local_' + Date.now(),
type: 'checkout.session.completed',
data: {
object: {
id: sessionId,
amount_total: this.getPriceAmount(priceId),
currency: 'usd',
customer: 'cus_local_' + Date.now(),
payment_status: 'paid',
status: 'complete',
metadata: { app_id: 'local-app-001', price_id: priceId }
}
}
};
// Send to webhook endpoint
const response = await fetch(`http://localhost:${this.port}/api/stripe/webhooks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event)
});
return response.ok;
}
getPriceAmount(priceId) {
const prices = {
'price_local_validate': 50000,
'price_local_prototype': 250000,
'price_local_strategy': 1000000,
'price_local_build': 2500000
};
return prices[priceId] || 10000;
}
setupCatchAll() {
// Catch-all for unimplemented routes
this.app.use((req, res) => {
res.status(404).json({
error: 'Not found',
message: `Endpoint ${req.method} ${req.path} not implemented in local mode`,
suggestion: 'Check /docs for available endpoints'
});
});
}
start() {
return new Promise((resolve, reject) => {
const server = this.app.listen(this.port, (err) => {
if (err) {
reject(err);
} else {
if (!this.json) {
console.log();
console.log(chalk.yellow('š SPAPS Local Development Server'));
console.log(chalk.green(`⨠Running at: http://localhost:${this.port}`));
console.log(chalk.blue(`š Documentation: http://localhost:${this.port}/docs`));
console.log(chalk.dim(' Press Ctrl+C to stop'));
console.log();
}
resolve(server);
}
});
// Handle errors
server.on('error', (err) => {
if (!this.json) {
if (err.code === 'EADDRINUSE') {
console.error(chalk.red(`ā Port ${this.port} is already in use`));
console.log(chalk.yellow('š” Try: spaps local --port 3301'));
} else {
console.error(chalk.red('ā Server error:'), err.message);
}
}
reject(err);
});
});
}
}
// Export for use in CLI
module.exports = LocalServer;
// Run directly if called as script
if (require.main === module) {
const server = new LocalServer();
server.start().catch(console.error);
// Graceful shutdown
process.on('SIGINT', () => {
console.log(chalk.yellow('\nš Shutting down...'));
process.exit(0);
});
}