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
JavaScript
/**
* 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;