spaps 0.3.4 → 0.3.6

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 +305 -55
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spaps",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
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,20 +152,74 @@ class LocalServer {
147
152
  });
148
153
  });
149
154
 
150
- // Mock Stripe checkout sessions endpoint (SDK expects this path)
151
- this.app.post('/api/stripe/checkout-sessions', (req, res) => {
152
- const sessionId = 'cs_local_' + Date.now();
153
- res.json({
154
- success: true,
155
- data: {
156
- sessionId,
157
- url: `http://localhost:${this.port}/checkout/${sessionId}?success=${encodeURIComponent(req.body.success_url)}&cancel=${encodeURIComponent(req.body.cancel_url)}`,
158
- amount_total: req.body.amount || 999,
159
- currency: req.body.currency || 'usd',
160
- payment_status: 'unpaid',
161
- status: 'open'
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
+ });
162
212
  }
163
- });
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
+ }
164
223
  });
165
224
 
166
225
  // Mock Stripe endpoints (legacy)
@@ -180,29 +239,77 @@ class LocalServer {
180
239
  });
181
240
  });
182
241
 
183
- // Mock Stripe products endpoint
184
- this.app.get('/api/stripe/products', (req, res) => {
185
- res.json({
186
- success: true,
187
- data: [
188
- {
189
- id: 'prod_local_validate',
190
- name: 'Validate',
191
- description: 'Proof of concept validation',
192
- price: 500,
193
- currency: 'usd',
194
- active: true
195
- },
196
- {
197
- id: 'prod_local_prototype',
198
- name: 'Prototype',
199
- description: 'Build an MVP prototype',
200
- price: 2500,
201
- currency: 'usd',
202
- active: true
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'
203
310
  }
204
- ]
205
- });
311
+ });
312
+ }
206
313
  });
207
314
 
208
315
  // Mock auth nonce endpoint
@@ -242,16 +349,111 @@ class LocalServer {
242
349
  });
243
350
  });
244
351
 
245
- // Mock admin product sync endpoint
246
- this.app.post('/api/v1/admin/products/sync', (req, res) => {
247
- res.json({
248
- success: true,
249
- message: 'Products synced successfully (local mode)',
250
- data: {
251
- synced_count: 2,
252
- products: ['Validate', 'Prototype']
352
+ // Admin product sync endpoint - REAL or MOCK based on config
353
+ this.app.post('/api/v1/admin/products/sync', async (req, res) => {
354
+ try {
355
+ if (USE_REAL_STRIPE) {
356
+ // Get local products from admin manager
357
+ const localProducts = this.adminManager.listProducts();
358
+ const syncResults = [];
359
+
360
+ for (const product of localProducts) {
361
+ try {
362
+ // Check if product already exists in Stripe
363
+ let stripeProduct;
364
+ try {
365
+ stripeProduct = await stripe.products.retrieve(product.id);
366
+ } catch (error) {
367
+ if (error.code === 'resource_missing') {
368
+ // Create new product in Stripe
369
+ stripeProduct = await stripe.products.create({
370
+ id: product.id,
371
+ name: product.name,
372
+ description: product.description,
373
+ metadata: {
374
+ spaps_managed: 'true',
375
+ created_by: 'spaps_admin'
376
+ }
377
+ });
378
+
379
+ // Create corresponding price
380
+ await stripe.prices.create({
381
+ id: product.price_id,
382
+ product: stripeProduct.id,
383
+ unit_amount: product.price,
384
+ currency: product.currency,
385
+ metadata: {
386
+ spaps_managed: 'true'
387
+ }
388
+ });
389
+
390
+ syncResults.push({
391
+ id: product.id,
392
+ name: product.name,
393
+ action: 'created',
394
+ stripe_id: stripeProduct.id
395
+ });
396
+ } else {
397
+ throw error;
398
+ }
399
+ }
400
+
401
+ if (stripeProduct && !syncResults.find(r => r.id === product.id)) {
402
+ // Update existing product
403
+ await stripe.products.update(stripeProduct.id, {
404
+ name: product.name,
405
+ description: product.description,
406
+ active: product.active
407
+ });
408
+
409
+ syncResults.push({
410
+ id: product.id,
411
+ name: product.name,
412
+ action: 'updated',
413
+ stripe_id: stripeProduct.id
414
+ });
415
+ }
416
+ } catch (productError) {
417
+ console.error(`Error syncing product ${product.id}:`, productError);
418
+ syncResults.push({
419
+ id: product.id,
420
+ name: product.name,
421
+ action: 'error',
422
+ error: productError.message
423
+ });
424
+ }
425
+ }
426
+
427
+ res.json({
428
+ success: true,
429
+ message: `Successfully synced ${syncResults.filter(r => r.action !== 'error').length} products to Stripe`,
430
+ data: {
431
+ synced_count: syncResults.filter(r => r.action !== 'error').length,
432
+ total_count: localProducts.length,
433
+ results: syncResults
434
+ }
435
+ });
436
+ } else {
437
+ // Mock response (fallback)
438
+ res.json({
439
+ success: true,
440
+ message: 'Products synced successfully (mock mode)',
441
+ data: {
442
+ synced_count: 2,
443
+ products: ['Validate', 'Prototype']
444
+ }
445
+ });
253
446
  }
254
- });
447
+ } catch (error) {
448
+ console.error('Product sync error:', error);
449
+ res.status(500).json({
450
+ success: false,
451
+ error: {
452
+ code: 'SYNC_ERROR',
453
+ message: error.message || 'Failed to sync products'
454
+ }
455
+ });
456
+ }
255
457
  });
256
458
 
257
459
  // Mock usage endpoints
@@ -389,19 +591,62 @@ class LocalServer {
389
591
  `);
390
592
  });
391
593
 
392
- // Mock webhook endpoint
594
+ // Stripe webhook endpoint - REAL or MOCK based on config
393
595
  this.app.post('/api/stripe/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
394
- // In local mode, accept all webhooks
395
- const event = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
396
-
397
- if (!this.json) {
398
- console.log(chalk.blue(`⚡ Webhook received: ${event.type}`));
596
+ try {
597
+ let event;
598
+
599
+ if (USE_REAL_STRIPE) {
600
+ // Real Stripe webhook verification
601
+ const sig = req.headers['stripe-signature'];
602
+ const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
603
+
604
+ if (webhookSecret && sig) {
605
+ try {
606
+ event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
607
+ } catch (err) {
608
+ console.error('Webhook signature verification failed:', err.message);
609
+ return res.status(400).send(`Webhook Error: ${err.message}`);
610
+ }
611
+ } else {
612
+ // For local development without webhook secret
613
+ event = JSON.parse(req.body.toString());
614
+ }
615
+ } else {
616
+ // Mock mode - accept all webhooks
617
+ event = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
618
+ }
619
+
620
+ if (!this.json) {
621
+ console.log(chalk.blue(`⚡ Webhook received: ${event.type}`));
622
+ }
623
+
624
+ // Handle the event
625
+ switch (event.type) {
626
+ case 'checkout.session.completed':
627
+ const session = event.data.object;
628
+ console.log(chalk.green(`✅ Payment successful: ${session.id}`));
629
+ break;
630
+ case 'payment_intent.succeeded':
631
+ const paymentIntent = event.data.object;
632
+ console.log(chalk.green(`💰 Payment intent succeeded: ${paymentIntent.id}`));
633
+ break;
634
+ case 'customer.subscription.created':
635
+ const subscription = event.data.object;
636
+ console.log(chalk.green(`📋 Subscription created: ${subscription.id}`));
637
+ break;
638
+ default:
639
+ console.log(chalk.yellow(`🔔 Unhandled event type: ${event.type}`));
640
+ }
641
+
642
+ // Store for testing
643
+ this.lastWebhookEvent = event;
644
+
645
+ res.json({ received: true });
646
+ } catch (error) {
647
+ console.error('Webhook processing error:', error);
648
+ res.status(500).json({ error: error.message });
399
649
  }
400
-
401
- // Store for testing
402
- this.lastWebhookEvent = event;
403
-
404
- res.json({ received: true });
405
650
  });
406
651
 
407
652
  // Webhook testing UI
@@ -787,6 +1032,11 @@ class LocalServer {
787
1032
  console.log(chalk.yellow('🍠 SPAPS Local Development Server'));
788
1033
  console.log(chalk.green(`✨ Running at: http://localhost:${this.port}`));
789
1034
  console.log(chalk.blue(`📝 Documentation: http://localhost:${this.port}/docs`));
1035
+ if (USE_REAL_STRIPE) {
1036
+ console.log(chalk.magenta('💳 Stripe: Real test mode (live API calls)'));
1037
+ } else {
1038
+ console.log(chalk.gray('💳 Stripe: Mock mode (simulated responses)'));
1039
+ }
790
1040
  console.log(chalk.dim(' Press Ctrl+C to stop'));
791
1041
  console.log();
792
1042
  }