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.
- package/package.json +5 -4
- 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.
|
|
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
|
-
"
|
|
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"
|
package/src/local-server.js
CHANGED
|
@@ -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
|
-
//
|
|
151
|
-
this.app.post('/api/stripe/checkout-sessions', (req, res) => {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
//
|
|
184
|
-
this.app.get('/api/stripe/products', (req, res) => {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
{
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
//
|
|
246
|
-
this.app.post('/api/v1/admin/products/sync', (req, res) => {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
}
|