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.
- package/package.json +5 -4
- 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
|
+
"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
|
-
"
|
|
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,7 +152,77 @@ class LocalServer {
|
|
|
147
152
|
});
|
|
148
153
|
});
|
|
149
154
|
|
|
150
|
-
//
|
|
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
|
|
247
|
-
this.app.
|
|
248
|
-
|
|
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
|
-
|
|
252
|
-
|
|
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
|
}
|