spaps 0.3.3 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +5 -4
  2. package/src/local-server.js +313 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spaps",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Sweet Potato Authentication & Payment Service CLI - Zero-config local development and project scaffolding",
5
5
  "main": "bin/spaps.js",
6
6
  "bin": {
@@ -37,13 +37,14 @@
37
37
  },
38
38
  "homepage": "https://sweetpotato.dev",
39
39
  "dependencies": {
40
+ "axios": "^1.6.0",
40
41
  "chalk": "^4.1.2",
41
42
  "commander": "^11.1.0",
43
+ "cors": "^2.8.5",
44
+ "express": "^4.18.2",
42
45
  "ora": "^5.4.1",
43
46
  "prompts": "^2.4.2",
44
- "axios": "^1.6.0",
45
- "express": "^4.18.2",
46
- "cors": "^2.8.5"
47
+ "stripe": "^18.5.0"
47
48
  },
48
49
  "engines": {
49
50
  "node": ">=16.0.0"
@@ -12,6 +12,11 @@ const { generateDocsHTML } = require('./docs-html');
12
12
  const StripeLocalManager = require('./stripe-local');
13
13
  const LocalAdminManager = require('./admin-local');
14
14
 
15
+ // Stripe configuration for test mode
16
+ const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY || 'sk_test_51S1WOy2HT0E1dOewiHvzt7T96PDwjocSDDUuc2ur569AVA5fDj4UpNM66lujrda1tTYrgooG0Z1dNFZfwEZuZdcA00nuVLJW67');
17
+ const STRIPE_PUBLISHABLE_KEY = process.env.STRIPE_PUBLISHABLE_KEY || 'pk_test_51S1WOy2HT0E1dOewb2EkxZIaPkz7v3zMM9VxuBoxgNILYMmS85I4zrAWTkevyUQcaWlWUoC2NYnB8X5ZKd5e7Ifc005IzIW6H2';
18
+ const USE_REAL_STRIPE = process.env.USE_REAL_STRIPE !== 'false'; // Default to true
19
+
15
20
  class LocalServer {
16
21
  constructor(options = {}) {
17
22
  this.port = options.port || process.env.PORT || 3456;
@@ -147,7 +152,77 @@ class LocalServer {
147
152
  });
148
153
  });
149
154
 
150
- // Mock Stripe endpoints
155
+ // Stripe checkout sessions endpoint - REAL or MOCK based on config
156
+ this.app.post('/api/stripe/checkout-sessions', async (req, res) => {
157
+ try {
158
+ if (USE_REAL_STRIPE) {
159
+ // Real Stripe checkout session
160
+ const { product_name, amount, currency = 'usd', success_url, cancel_url, price_id } = req.body;
161
+
162
+ let lineItems;
163
+ if (price_id) {
164
+ // Use existing price
165
+ lineItems = [{ price: price_id, quantity: 1 }];
166
+ } else {
167
+ // Create price on the fly
168
+ lineItems = [{
169
+ price_data: {
170
+ currency,
171
+ product_data: { name: product_name || 'Product' },
172
+ unit_amount: amount || 999
173
+ },
174
+ quantity: 1
175
+ }];
176
+ }
177
+
178
+ const session = await stripe.checkout.sessions.create({
179
+ mode: 'payment',
180
+ line_items: lineItems,
181
+ success_url,
182
+ cancel_url,
183
+ automatic_tax: { enabled: false },
184
+ customer_creation: 'always'
185
+ });
186
+
187
+ res.json({
188
+ success: true,
189
+ data: {
190
+ sessionId: session.id,
191
+ url: session.url,
192
+ amount_total: session.amount_total,
193
+ currency: session.currency,
194
+ payment_status: session.payment_status,
195
+ status: session.status
196
+ }
197
+ });
198
+ } else {
199
+ // Mock response (fallback)
200
+ const sessionId = 'cs_local_' + Date.now();
201
+ res.json({
202
+ success: true,
203
+ data: {
204
+ sessionId,
205
+ url: `http://localhost:${this.port}/checkout/${sessionId}?success=${encodeURIComponent(req.body.success_url)}&cancel=${encodeURIComponent(req.body.cancel_url)}`,
206
+ amount_total: req.body.amount || 999,
207
+ currency: req.body.currency || 'usd',
208
+ payment_status: 'unpaid',
209
+ status: 'open'
210
+ }
211
+ });
212
+ }
213
+ } catch (error) {
214
+ console.error('Stripe checkout error:', error);
215
+ res.status(500).json({
216
+ success: false,
217
+ error: {
218
+ code: 'CHECKOUT_ERROR',
219
+ message: error.message || 'Failed to create checkout session'
220
+ }
221
+ });
222
+ }
223
+ });
224
+
225
+ // Mock Stripe endpoints (legacy)
151
226
  this.app.post('/api/stripe/create-checkout-session', (req, res) => {
152
227
  res.json({
153
228
  sessionId: 'cs_test_local_' + Date.now(),
@@ -164,6 +239,128 @@ class LocalServer {
164
239
  });
165
240
  });
166
241
 
242
+ // Stripe products endpoint - REAL or MOCK based on config
243
+ this.app.get('/api/stripe/products', async (req, res) => {
244
+ try {
245
+ if (USE_REAL_STRIPE) {
246
+ // Fetch real Stripe products
247
+ const products = await stripe.products.list({
248
+ active: req.query.active !== undefined ? req.query.active === 'true' : undefined,
249
+ limit: req.query.limit ? parseInt(req.query.limit) : 10
250
+ });
251
+
252
+ // Get prices for each product
253
+ const productsWithPrices = await Promise.all(
254
+ products.data.map(async (product) => {
255
+ const prices = await stripe.prices.list({
256
+ product: product.id,
257
+ active: true,
258
+ limit: 1
259
+ });
260
+
261
+ const defaultPrice = prices.data[0];
262
+ return {
263
+ id: product.id,
264
+ name: product.name,
265
+ description: product.description,
266
+ price: defaultPrice ? defaultPrice.unit_amount : 0,
267
+ currency: defaultPrice ? defaultPrice.currency : 'usd',
268
+ price_id: defaultPrice ? defaultPrice.id : null,
269
+ active: product.active,
270
+ metadata: product.metadata
271
+ };
272
+ })
273
+ );
274
+
275
+ res.json({
276
+ success: true,
277
+ data: productsWithPrices
278
+ });
279
+ } else {
280
+ // Mock response (fallback)
281
+ res.json({
282
+ success: true,
283
+ data: [
284
+ {
285
+ id: 'prod_local_validate',
286
+ name: 'Validate',
287
+ description: 'Proof of concept validation',
288
+ price: 500,
289
+ currency: 'usd',
290
+ active: true
291
+ },
292
+ {
293
+ id: 'prod_local_prototype',
294
+ name: 'Prototype',
295
+ description: 'Build an MVP prototype',
296
+ price: 2500,
297
+ currency: 'usd',
298
+ active: true
299
+ }
300
+ ]
301
+ });
302
+ }
303
+ } catch (error) {
304
+ console.error('Stripe products error:', error);
305
+ res.status(500).json({
306
+ success: false,
307
+ error: {
308
+ code: 'PRODUCTS_ERROR',
309
+ message: error.message || 'Failed to fetch products'
310
+ }
311
+ });
312
+ }
313
+ });
314
+
315
+ // Mock auth nonce endpoint
316
+ this.app.post('/api/auth/nonce', (req, res) => {
317
+ const { wallet_address } = req.body;
318
+ res.json({
319
+ success: true,
320
+ data: {
321
+ nonce: 'local-nonce-' + Date.now(),
322
+ message: `Sign this message to authenticate your wallet ${wallet_address}.\n\nNonce: local-nonce-${Date.now()}`,
323
+ wallet_address,
324
+ expires_at: new Date(Date.now() + 300000).toISOString()
325
+ }
326
+ });
327
+ });
328
+
329
+ // Mock magic link endpoint
330
+ this.app.post('/api/auth/magic-link', (req, res) => {
331
+ const { email } = req.body;
332
+ res.json({
333
+ success: true,
334
+ message: 'Magic link sent successfully (simulated in local mode)',
335
+ data: {
336
+ email,
337
+ sent_at: new Date().toISOString()
338
+ }
339
+ });
340
+ });
341
+
342
+ // Mock customer portal endpoint
343
+ this.app.post('/api/stripe/customer-portal', (req, res) => {
344
+ res.json({
345
+ success: true,
346
+ data: {
347
+ url: `http://localhost:${this.port}/customer-portal?return=${encodeURIComponent(req.body.return_url || 'http://localhost:3000')}`
348
+ }
349
+ });
350
+ });
351
+
352
+ // Mock admin product sync endpoint
353
+ this.app.post('/api/v1/admin/products/sync', (req, res) => {
354
+ res.json({
355
+ success: true,
356
+ message: 'Products synced successfully (local mode)',
357
+ data: {
358
+ synced_count: 2,
359
+ products: ['Validate', 'Prototype']
360
+ }
361
+ });
362
+ });
363
+
167
364
  // Mock usage endpoints
168
365
  this.app.get('/api/usage/balance', (req, res) => {
169
366
  res.json({
@@ -243,19 +440,118 @@ class LocalServer {
243
440
  `);
244
441
  });
245
442
 
246
- // Mock webhook endpoint
247
- this.app.post('/api/stripe/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
248
- // In local mode, accept all webhooks
249
- const event = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
443
+ // Mock customer portal page
444
+ this.app.get('/customer-portal', (req, res) => {
445
+ const { return: returnUrl } = req.query;
250
446
 
251
- if (!this.json) {
252
- console.log(chalk.blue(`⚡ Webhook received: ${event.type}`));
447
+ res.send(`
448
+ <!DOCTYPE html>
449
+ <html>
450
+ <head>
451
+ <title>SPAPS Local - Customer Portal</title>
452
+ <style>
453
+ body { font-family: system-ui; max-width: 600px; margin: 100px auto; padding: 2rem; }
454
+ button { width: 100%; padding: 1rem; margin: 0.5rem 0; border: none; border-radius: 8px; cursor: pointer; font-size: 16px; }
455
+ .primary { background: #635bff; color: white; }
456
+ .primary:hover { background: #4b41e0; }
457
+ .secondary { background: #f5f5f5; }
458
+ .secondary:hover { background: #e5e5e5; }
459
+ .section { background: #f9f9f9; padding: 1.5rem; margin: 1rem 0; border-radius: 8px; }
460
+ </style>
461
+ </head>
462
+ <body>
463
+ <h1>🍠 Customer Portal (Local)</h1>
464
+ <p>Manage your subscription and billing information.</p>
465
+
466
+ <div class="section">
467
+ <h3>Current Subscription</h3>
468
+ <p><strong>Plan:</strong> Premium Plan</p>
469
+ <p><strong>Status:</strong> Active</p>
470
+ <p><strong>Next billing:</strong> ${new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toLocaleDateString()}</p>
471
+ </div>
472
+
473
+ <div class="section">
474
+ <h3>Payment Method</h3>
475
+ <p><strong>Card:</strong> •••• •••• •••• 4242</p>
476
+ <p><strong>Expires:</strong> 12/2025</p>
477
+ <button class="secondary">Update Payment Method</button>
478
+ </div>
479
+
480
+ <div class="section">
481
+ <h3>Billing History</h3>
482
+ <p>• $25.00 - Premium Plan (${new Date().toLocaleDateString()})</p>
483
+ <p>• $25.00 - Premium Plan (${new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toLocaleDateString()})</p>
484
+ <button class="secondary">Download All Invoices</button>
485
+ </div>
486
+
487
+ <button class="primary" onclick="window.location='${returnUrl || 'http://localhost:3000'}'">
488
+ Return to Application
489
+ </button>
490
+
491
+ <p style="margin-top: 2rem; color: #666; font-size: 14px;">
492
+ This is a mock customer portal for local development.
493
+ </p>
494
+ </body>
495
+ </html>
496
+ `);
497
+ });
498
+
499
+ // Stripe webhook endpoint - REAL or MOCK based on config
500
+ this.app.post('/api/stripe/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
501
+ try {
502
+ let event;
503
+
504
+ if (USE_REAL_STRIPE) {
505
+ // Real Stripe webhook verification
506
+ const sig = req.headers['stripe-signature'];
507
+ const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
508
+
509
+ if (webhookSecret && sig) {
510
+ try {
511
+ event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
512
+ } catch (err) {
513
+ console.error('Webhook signature verification failed:', err.message);
514
+ return res.status(400).send(`Webhook Error: ${err.message}`);
515
+ }
516
+ } else {
517
+ // For local development without webhook secret
518
+ event = JSON.parse(req.body.toString());
519
+ }
520
+ } else {
521
+ // Mock mode - accept all webhooks
522
+ event = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
523
+ }
524
+
525
+ if (!this.json) {
526
+ console.log(chalk.blue(`⚡ Webhook received: ${event.type}`));
527
+ }
528
+
529
+ // Handle the event
530
+ switch (event.type) {
531
+ case 'checkout.session.completed':
532
+ const session = event.data.object;
533
+ console.log(chalk.green(`✅ Payment successful: ${session.id}`));
534
+ break;
535
+ case 'payment_intent.succeeded':
536
+ const paymentIntent = event.data.object;
537
+ console.log(chalk.green(`💰 Payment intent succeeded: ${paymentIntent.id}`));
538
+ break;
539
+ case 'customer.subscription.created':
540
+ const subscription = event.data.object;
541
+ console.log(chalk.green(`📋 Subscription created: ${subscription.id}`));
542
+ break;
543
+ default:
544
+ console.log(chalk.yellow(`🔔 Unhandled event type: ${event.type}`));
545
+ }
546
+
547
+ // Store for testing
548
+ this.lastWebhookEvent = event;
549
+
550
+ res.json({ received: true });
551
+ } catch (error) {
552
+ console.error('Webhook processing error:', error);
553
+ res.status(500).json({ error: error.message });
253
554
  }
254
-
255
- // Store for testing
256
- this.lastWebhookEvent = event;
257
-
258
- res.json({ received: true });
259
555
  });
260
556
 
261
557
  // Webhook testing UI
@@ -641,6 +937,11 @@ class LocalServer {
641
937
  console.log(chalk.yellow('🍠 SPAPS Local Development Server'));
642
938
  console.log(chalk.green(`✨ Running at: http://localhost:${this.port}`));
643
939
  console.log(chalk.blue(`📝 Documentation: http://localhost:${this.port}/docs`));
940
+ if (USE_REAL_STRIPE) {
941
+ console.log(chalk.magenta('💳 Stripe: Real test mode (live API calls)'));
942
+ } else {
943
+ console.log(chalk.gray('💳 Stripe: Mock mode (simulated responses)'));
944
+ }
644
945
  console.log(chalk.dim(' Press Ctrl+C to stop'));
645
946
  console.log();
646
947
  }