UNPKG

spaps

Version:

Sweet Potato Authentication & Payment Service CLI - Zero-config local development with built-in admin middleware and permission utilities

263 lines (229 loc) • 8.11 kB
/** * Local Stripe Integration for SPAPS CLI * Handles webhook forwarding and testing without Stripe CLI */ const { spawn } = require('child_process'); const chalk = require('chalk'); const ora = require('ora'); const fs = require('fs'); const path = require('path'); class StripeLocalManager { constructor(options = {}) { this.port = options.port || 3300; this.stripeCliProcess = null; this.useBuiltInSimulator = options.simulator !== false; this.webhookSecret = 'whsec_local_development_secret'; } /** * Check if Stripe CLI is installed */ async checkStripeCLI() { try { const { execSync } = require('child_process'); execSync('stripe --version', { stdio: 'ignore' }); return true; } catch (error) { return false; } } /** * Start Stripe webhook forwarding */ async startWebhookForwarding() { const hasStripeCLI = await this.checkStripeCLI(); if (hasStripeCLI && !this.useBuiltInSimulator) { return this.startStripeCLI(); } else { return this.startBuiltInSimulator(); } } /** * Start Stripe CLI webhook forwarding */ async startStripeCLI() { console.log(chalk.blue('\nšŸ“” Starting Stripe CLI webhook forwarding...')); return new Promise((resolve, reject) => { this.stripeCliProcess = spawn('stripe', [ 'listen', '--forward-to', `localhost:${this.port}/api/stripe/webhooks`, '--print-json' ]); let webhookSecret = null; this.stripeCliProcess.stdout.on('data', (data) => { const output = data.toString(); // Parse webhook secret from output if (!webhookSecret && output.includes('whsec_')) { const match = output.match(/whsec_[a-zA-Z0-9]+/); if (match) { webhookSecret = match[0]; console.log(chalk.green(`āœ… Stripe webhooks connected!`)); console.log(chalk.gray(` Secret: ${webhookSecret}`)); console.log(chalk.gray(` Forwarding to: http://localhost:${this.port}/api/stripe/webhooks`)); // Save webhook secret to env file this.saveWebhookSecret(webhookSecret); resolve({ type: 'stripe-cli', secret: webhookSecret, url: `http://localhost:${this.port}/api/stripe/webhooks` }); } } // Log webhook events try { const json = JSON.parse(output); if (json.type) { console.log(chalk.blue(`⚔ Webhook: ${json.type}`)); } } catch (e) { // Not JSON, ignore } }); this.stripeCliProcess.stderr.on('data', (data) => { const error = data.toString(); if (error.includes('login')) { console.log(chalk.yellow('\nāš ļø Stripe CLI not logged in')); console.log(chalk.cyan(' Run: stripe login')); reject(new Error('Stripe CLI not authenticated')); } else if (error.includes('Error')) { console.error(chalk.red(`Stripe CLI error: ${error}`)); } }); this.stripeCliProcess.on('close', (code) => { if (code !== 0 && code !== null) { reject(new Error(`Stripe CLI exited with code ${code}`)); } }); }); } /** * Start built-in webhook simulator */ async startBuiltInSimulator() { console.log(chalk.blue('\nšŸŽ­ Starting built-in webhook simulator...')); console.log(chalk.gray(' (Stripe CLI not found or simulator mode enabled)')); // The local server will handle webhook simulation console.log(chalk.green(`āœ… Webhook simulator ready!`)); console.log(chalk.gray(` Test UI: http://localhost:${this.port}/api/stripe/webhooks/test`)); console.log(chalk.gray(` Endpoint: http://localhost:${this.port}/api/stripe/webhooks`)); return { type: 'simulator', secret: this.webhookSecret, url: `http://localhost:${this.port}/api/stripe/webhooks`, testUI: `http://localhost:${this.port}/api/stripe/webhooks/test` }; } /** * Save webhook secret to local env file */ saveWebhookSecret(secret) { const envPath = path.join(process.cwd(), '.env.local'); try { let envContent = ''; if (fs.existsSync(envPath)) { envContent = fs.readFileSync(envPath, 'utf8'); } // Update or add webhook secret if (envContent.includes('STRIPE_WEBHOOK_SECRET=')) { envContent = envContent.replace( /STRIPE_WEBHOOK_SECRET=.*/, `STRIPE_WEBHOOK_SECRET=${secret}` ); } else { envContent += `\n# Auto-generated by SPAPS\nSTRIPE_WEBHOOK_SECRET=${secret}\n`; } fs.writeFileSync(envPath, envContent); console.log(chalk.gray(` Secret saved to .env.local`)); } catch (error) { console.error(chalk.yellow(` Could not save webhook secret: ${error.message}`)); } } /** * Stop webhook forwarding */ stop() { if (this.stripeCliProcess) { console.log(chalk.yellow('\nšŸ‘‹ Stopping Stripe webhook forwarding...')); this.stripeCliProcess.kill(); this.stripeCliProcess = null; } } /** * Create test products for local development */ async createTestProducts() { console.log(chalk.blue('\nšŸ“¦ Creating test Stripe products...')); const products = [ { id: 'prod_local_validate', name: 'Validate Tier', description: 'Landing page with data capture', price: 50000, // $500 price_id: 'price_local_validate' }, { id: 'prod_local_prototype', name: 'Prototype Tier', description: 'Clickable prototype with core flows', price: 250000, // $2,500 price_id: 'price_local_prototype' }, { id: 'prod_local_strategy', name: 'Strategy Tier', description: 'Technical architecture and roadmap', price: 1000000, // $10,000 price_id: 'price_local_strategy' }, { id: 'prod_local_build', name: 'Build Tier', description: 'Full application development', price: 2500000, // $25,000 price_id: 'price_local_build' } ]; // Store products in local config const configPath = path.join(process.cwd(), '.spaps', 'stripe-products.json'); const configDir = path.dirname(configPath); if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir, { recursive: true }); } fs.writeFileSync(configPath, JSON.stringify(products, null, 2)); console.log(chalk.green('āœ… Test products created:')); products.forEach(p => { console.log(chalk.gray(` - ${p.name}: $${p.price / 100}`)); }); return products; } /** * Show webhook testing instructions */ showInstructions() { console.log(chalk.yellow('\nšŸ“š Webhook Testing Guide:')); console.log(); if (this.useBuiltInSimulator) { console.log('1. Open webhook tester UI:'); console.log(chalk.cyan(` http://localhost:${this.port}/api/stripe/webhooks/test`)); console.log(); console.log('2. Or trigger via code:'); console.log(chalk.gray(' ```javascript')); console.log(chalk.gray(' // Your app code')); console.log(chalk.gray(' const result = await spaps.createCheckoutSession(...);')); console.log(chalk.gray(' // Webhook fires automatically after 1 second')); console.log(chalk.gray(' ```')); } else { console.log('1. Trigger test events:'); console.log(chalk.cyan(' stripe trigger payment_intent.succeeded')); console.log(); console.log('2. Or use the Stripe Dashboard:'); console.log(chalk.cyan(' https://dashboard.stripe.com/test/webhooks')); } console.log(); console.log(chalk.blue('šŸ’” Tips:')); console.log(' - Webhooks auto-retry on failure'); console.log(' - Check logs for webhook events'); console.log(' - Use webhook secret in your app'); } } module.exports = StripeLocalManager;