mcp-twin 1.2.0 → 1.4.0

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 (46) hide show
  1. package/.env.example +30 -0
  2. package/PRD.md +682 -0
  3. package/dist/cli.js +41 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/cloud/auth.d.ts +108 -0
  6. package/dist/cloud/auth.d.ts.map +1 -0
  7. package/dist/cloud/auth.js +199 -0
  8. package/dist/cloud/auth.js.map +1 -0
  9. package/dist/cloud/db.d.ts +21 -0
  10. package/dist/cloud/db.d.ts.map +1 -0
  11. package/dist/cloud/db.js +158 -0
  12. package/dist/cloud/db.js.map +1 -0
  13. package/dist/cloud/routes/auth.d.ts +7 -0
  14. package/dist/cloud/routes/auth.d.ts.map +1 -0
  15. package/dist/cloud/routes/auth.js +291 -0
  16. package/dist/cloud/routes/auth.js.map +1 -0
  17. package/dist/cloud/routes/billing.d.ts +7 -0
  18. package/dist/cloud/routes/billing.d.ts.map +1 -0
  19. package/dist/cloud/routes/billing.js +368 -0
  20. package/dist/cloud/routes/billing.js.map +1 -0
  21. package/dist/cloud/routes/twins.d.ts +7 -0
  22. package/dist/cloud/routes/twins.d.ts.map +1 -0
  23. package/dist/cloud/routes/twins.js +740 -0
  24. package/dist/cloud/routes/twins.js.map +1 -0
  25. package/dist/cloud/routes/usage.d.ts +7 -0
  26. package/dist/cloud/routes/usage.d.ts.map +1 -0
  27. package/dist/cloud/routes/usage.js +145 -0
  28. package/dist/cloud/routes/usage.js.map +1 -0
  29. package/dist/cloud/server.d.ts +10 -0
  30. package/dist/cloud/server.d.ts.map +1 -0
  31. package/dist/cloud/server.js +161 -0
  32. package/dist/cloud/server.js.map +1 -0
  33. package/dist/cloud/stripe.d.ts +60 -0
  34. package/dist/cloud/stripe.d.ts.map +1 -0
  35. package/dist/cloud/stripe.js +157 -0
  36. package/dist/cloud/stripe.js.map +1 -0
  37. package/package.json +25 -4
  38. package/src/cli.ts +10 -0
  39. package/src/cloud/auth.ts +269 -0
  40. package/src/cloud/db.ts +167 -0
  41. package/src/cloud/routes/auth.ts +355 -0
  42. package/src/cloud/routes/billing.ts +460 -0
  43. package/src/cloud/routes/twins.ts +908 -0
  44. package/src/cloud/routes/usage.ts +186 -0
  45. package/src/cloud/server.ts +171 -0
  46. package/src/cloud/stripe.ts +192 -0
@@ -0,0 +1,460 @@
1
+ /**
2
+ * Billing Routes
3
+ * MCP Twin Cloud
4
+ */
5
+
6
+ import { Router, Request, Response } from 'express';
7
+ import { query, queryOne } from '../db';
8
+ import { authenticateApiKey, User } from '../auth';
9
+ import {
10
+ isStripeConfigured,
11
+ getOrCreateCustomer,
12
+ createCheckoutSession,
13
+ createPortalSession,
14
+ getInvoices,
15
+ constructWebhookEvent,
16
+ TIER_TO_PRICE,
17
+ PRICE_TO_TIER,
18
+ } from '../stripe';
19
+
20
+ const router = Router();
21
+
22
+ /**
23
+ * GET /api/billing/status
24
+ * Get billing status and current plan
25
+ */
26
+ router.get('/status', authenticateApiKey, async (req: Request, res: Response) => {
27
+ try {
28
+ if (!isStripeConfigured()) {
29
+ res.json({
30
+ configured: false,
31
+ message: 'Stripe is not configured. Set STRIPE_SECRET_KEY to enable billing.',
32
+ });
33
+ return;
34
+ }
35
+
36
+ const user = await queryOne<User>(
37
+ 'SELECT id, email, tier, stripe_customer_id, stripe_subscription_id FROM users WHERE id = $1',
38
+ [req.user!.id]
39
+ );
40
+
41
+ if (!user) {
42
+ res.status(404).json({
43
+ error: { code: 'NOT_FOUND', message: 'User not found' },
44
+ });
45
+ return;
46
+ }
47
+
48
+ res.json({
49
+ configured: true,
50
+ tier: user.tier,
51
+ hasSubscription: !!user.stripe_subscription_id,
52
+ customerId: user.stripe_customer_id ? `cus_...${user.stripe_customer_id.slice(-4)}` : null,
53
+ });
54
+ } catch (error: any) {
55
+ console.error('[Billing] Status error:', error);
56
+ res.status(500).json({
57
+ error: { code: 'INTERNAL_ERROR', message: 'Failed to get billing status' },
58
+ });
59
+ }
60
+ });
61
+
62
+ /**
63
+ * GET /api/billing/plans
64
+ * Get available plans
65
+ */
66
+ router.get('/plans', async (req: Request, res: Response) => {
67
+ res.json({
68
+ plans: [
69
+ {
70
+ id: 'free',
71
+ name: 'Free',
72
+ price: 0,
73
+ interval: null,
74
+ features: [
75
+ '1 twin',
76
+ '10,000 requests/month',
77
+ '24-hour log retention',
78
+ 'Community support',
79
+ ],
80
+ },
81
+ {
82
+ id: 'starter',
83
+ name: 'Starter',
84
+ price: 29,
85
+ interval: 'month',
86
+ priceId: TIER_TO_PRICE.starter,
87
+ features: [
88
+ '5 twins',
89
+ '100,000 requests/month',
90
+ '7-day log retention',
91
+ 'Email support',
92
+ ],
93
+ },
94
+ {
95
+ id: 'pro',
96
+ name: 'Professional',
97
+ price: 149,
98
+ interval: 'month',
99
+ priceId: TIER_TO_PRICE.pro,
100
+ features: [
101
+ 'Unlimited twins',
102
+ '1,000,000 requests/month',
103
+ '30-day log retention',
104
+ 'Priority support',
105
+ 'Custom integrations',
106
+ ],
107
+ },
108
+ {
109
+ id: 'enterprise',
110
+ name: 'Enterprise',
111
+ price: null,
112
+ interval: null,
113
+ features: [
114
+ 'Unlimited everything',
115
+ '90-day log retention',
116
+ 'Dedicated support',
117
+ 'SLA guarantee',
118
+ 'SSO/SAML',
119
+ 'Custom contracts',
120
+ ],
121
+ contactSales: true,
122
+ },
123
+ ],
124
+ });
125
+ });
126
+
127
+ /**
128
+ * POST /api/billing/checkout
129
+ * Create a Stripe checkout session
130
+ */
131
+ router.post('/checkout', authenticateApiKey, async (req: Request, res: Response) => {
132
+ try {
133
+ if (!isStripeConfigured()) {
134
+ res.status(400).json({
135
+ error: {
136
+ code: 'STRIPE_NOT_CONFIGURED',
137
+ message: 'Stripe is not configured',
138
+ },
139
+ });
140
+ return;
141
+ }
142
+
143
+ const { plan, successUrl, cancelUrl } = req.body;
144
+
145
+ if (!plan || !successUrl || !cancelUrl) {
146
+ res.status(400).json({
147
+ error: {
148
+ code: 'VALIDATION_ERROR',
149
+ message: 'plan, successUrl, and cancelUrl are required',
150
+ },
151
+ });
152
+ return;
153
+ }
154
+
155
+ const priceId = TIER_TO_PRICE[plan];
156
+ if (!priceId) {
157
+ res.status(400).json({
158
+ error: {
159
+ code: 'INVALID_PLAN',
160
+ message: `Invalid plan: ${plan}. Valid plans: starter, pro`,
161
+ },
162
+ });
163
+ return;
164
+ }
165
+
166
+ const user = await queryOne<User>(
167
+ 'SELECT id, email, stripe_customer_id FROM users WHERE id = $1',
168
+ [req.user!.id]
169
+ );
170
+
171
+ if (!user) {
172
+ res.status(404).json({
173
+ error: { code: 'NOT_FOUND', message: 'User not found' },
174
+ });
175
+ return;
176
+ }
177
+
178
+ // Get or create Stripe customer
179
+ const customerId = await getOrCreateCustomer(
180
+ user.id,
181
+ user.email,
182
+ user.stripe_customer_id
183
+ );
184
+
185
+ // Update user with customer ID if new
186
+ if (!user.stripe_customer_id) {
187
+ await query(
188
+ 'UPDATE users SET stripe_customer_id = $1 WHERE id = $2',
189
+ [customerId, user.id]
190
+ );
191
+ }
192
+
193
+ // Create checkout session
194
+ const session = await createCheckoutSession(
195
+ customerId,
196
+ priceId,
197
+ successUrl,
198
+ cancelUrl
199
+ );
200
+
201
+ res.json({
202
+ sessionId: session.id,
203
+ url: session.url,
204
+ });
205
+ } catch (error: any) {
206
+ console.error('[Billing] Checkout error:', error);
207
+ res.status(500).json({
208
+ error: { code: 'INTERNAL_ERROR', message: 'Failed to create checkout session' },
209
+ });
210
+ }
211
+ });
212
+
213
+ /**
214
+ * POST /api/billing/portal
215
+ * Create a Stripe billing portal session
216
+ */
217
+ router.post('/portal', authenticateApiKey, async (req: Request, res: Response) => {
218
+ try {
219
+ if (!isStripeConfigured()) {
220
+ res.status(400).json({
221
+ error: {
222
+ code: 'STRIPE_NOT_CONFIGURED',
223
+ message: 'Stripe is not configured',
224
+ },
225
+ });
226
+ return;
227
+ }
228
+
229
+ const { returnUrl } = req.body;
230
+
231
+ if (!returnUrl) {
232
+ res.status(400).json({
233
+ error: {
234
+ code: 'VALIDATION_ERROR',
235
+ message: 'returnUrl is required',
236
+ },
237
+ });
238
+ return;
239
+ }
240
+
241
+ const user = await queryOne<User>(
242
+ 'SELECT stripe_customer_id FROM users WHERE id = $1',
243
+ [req.user!.id]
244
+ );
245
+
246
+ if (!user?.stripe_customer_id) {
247
+ res.status(400).json({
248
+ error: {
249
+ code: 'NO_SUBSCRIPTION',
250
+ message: 'No billing account found. Subscribe to a plan first.',
251
+ },
252
+ });
253
+ return;
254
+ }
255
+
256
+ const session = await createPortalSession(user.stripe_customer_id, returnUrl);
257
+
258
+ res.json({
259
+ url: session.url,
260
+ });
261
+ } catch (error: any) {
262
+ console.error('[Billing] Portal error:', error);
263
+ res.status(500).json({
264
+ error: { code: 'INTERNAL_ERROR', message: 'Failed to create portal session' },
265
+ });
266
+ }
267
+ });
268
+
269
+ /**
270
+ * GET /api/billing/invoices
271
+ * Get user's invoices
272
+ */
273
+ router.get('/invoices', authenticateApiKey, async (req: Request, res: Response) => {
274
+ try {
275
+ if (!isStripeConfigured()) {
276
+ res.json({ invoices: [] });
277
+ return;
278
+ }
279
+
280
+ const user = await queryOne<User>(
281
+ 'SELECT stripe_customer_id FROM users WHERE id = $1',
282
+ [req.user!.id]
283
+ );
284
+
285
+ if (!user?.stripe_customer_id) {
286
+ res.json({ invoices: [] });
287
+ return;
288
+ }
289
+
290
+ const invoices = await getInvoices(user.stripe_customer_id);
291
+
292
+ res.json({
293
+ invoices: invoices.map((inv) => ({
294
+ id: inv.id,
295
+ number: inv.number,
296
+ status: inv.status,
297
+ amount: inv.amount_due / 100,
298
+ currency: inv.currency,
299
+ created: new Date(inv.created * 1000).toISOString(),
300
+ pdfUrl: inv.invoice_pdf,
301
+ hostedUrl: inv.hosted_invoice_url,
302
+ })),
303
+ });
304
+ } catch (error: any) {
305
+ console.error('[Billing] Invoices error:', error);
306
+ res.status(500).json({
307
+ error: { code: 'INTERNAL_ERROR', message: 'Failed to get invoices' },
308
+ });
309
+ }
310
+ });
311
+
312
+ /**
313
+ * POST /api/billing/webhook
314
+ * Handle Stripe webhooks
315
+ */
316
+ router.post(
317
+ '/webhook',
318
+ // Use raw body for webhook signature verification
319
+ async (req: Request, res: Response) => {
320
+ const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
321
+
322
+ if (!webhookSecret) {
323
+ console.error('[Billing] STRIPE_WEBHOOK_SECRET not configured');
324
+ res.status(400).json({ error: 'Webhook not configured' });
325
+ return;
326
+ }
327
+
328
+ const signature = req.headers['stripe-signature'] as string;
329
+
330
+ if (!signature) {
331
+ res.status(400).json({ error: 'Missing stripe-signature header' });
332
+ return;
333
+ }
334
+
335
+ let event;
336
+
337
+ try {
338
+ // req.body should be raw buffer for webhook verification
339
+ const rawBody = req.body;
340
+ event = constructWebhookEvent(rawBody, signature, webhookSecret);
341
+ } catch (err: any) {
342
+ console.error('[Billing] Webhook signature verification failed:', err.message);
343
+ res.status(400).json({ error: `Webhook Error: ${err.message}` });
344
+ return;
345
+ }
346
+
347
+ console.log(`[Billing] Webhook received: ${event.type}`);
348
+
349
+ try {
350
+ switch (event.type) {
351
+ case 'checkout.session.completed': {
352
+ const session = event.data.object as any;
353
+ await handleCheckoutComplete(session);
354
+ break;
355
+ }
356
+
357
+ case 'customer.subscription.created':
358
+ case 'customer.subscription.updated': {
359
+ const subscription = event.data.object as any;
360
+ await handleSubscriptionUpdate(subscription);
361
+ break;
362
+ }
363
+
364
+ case 'customer.subscription.deleted': {
365
+ const subscription = event.data.object as any;
366
+ await handleSubscriptionDeleted(subscription);
367
+ break;
368
+ }
369
+
370
+ case 'invoice.payment_failed': {
371
+ const invoice = event.data.object as any;
372
+ await handlePaymentFailed(invoice);
373
+ break;
374
+ }
375
+
376
+ default:
377
+ console.log(`[Billing] Unhandled event type: ${event.type}`);
378
+ }
379
+
380
+ res.json({ received: true });
381
+ } catch (error: any) {
382
+ console.error('[Billing] Webhook handler error:', error);
383
+ res.status(500).json({ error: 'Webhook handler failed' });
384
+ }
385
+ }
386
+ );
387
+
388
+ /**
389
+ * Handle checkout.session.completed
390
+ */
391
+ async function handleCheckoutComplete(session: any): Promise<void> {
392
+ const customerId = session.customer;
393
+ const subscriptionId = session.subscription;
394
+
395
+ console.log(`[Billing] Checkout complete for customer ${customerId}`);
396
+
397
+ // Update user with subscription ID
398
+ await query(
399
+ `UPDATE users
400
+ SET stripe_subscription_id = $1, updated_at = NOW()
401
+ WHERE stripe_customer_id = $2`,
402
+ [subscriptionId, customerId]
403
+ );
404
+ }
405
+
406
+ /**
407
+ * Handle subscription created/updated
408
+ */
409
+ async function handleSubscriptionUpdate(subscription: any): Promise<void> {
410
+ const customerId = subscription.customer;
411
+ const status = subscription.status;
412
+ const priceId = subscription.items.data[0]?.price?.id;
413
+
414
+ console.log(`[Billing] Subscription update: ${customerId} -> ${status} (${priceId})`);
415
+
416
+ if (status === 'active' || status === 'trialing') {
417
+ // Determine tier from price
418
+ const tier = PRICE_TO_TIER[priceId] || 'free';
419
+
420
+ await query(
421
+ `UPDATE users
422
+ SET tier = $1, stripe_subscription_id = $2, updated_at = NOW()
423
+ WHERE stripe_customer_id = $3`,
424
+ [tier, subscription.id, customerId]
425
+ );
426
+
427
+ console.log(`[Billing] User upgraded to ${tier}`);
428
+ }
429
+ }
430
+
431
+ /**
432
+ * Handle subscription deleted
433
+ */
434
+ async function handleSubscriptionDeleted(subscription: any): Promise<void> {
435
+ const customerId = subscription.customer;
436
+
437
+ console.log(`[Billing] Subscription deleted for ${customerId}`);
438
+
439
+ // Downgrade to free tier
440
+ await query(
441
+ `UPDATE users
442
+ SET tier = 'free', stripe_subscription_id = NULL, updated_at = NOW()
443
+ WHERE stripe_customer_id = $1`,
444
+ [customerId]
445
+ );
446
+ }
447
+
448
+ /**
449
+ * Handle payment failed
450
+ */
451
+ async function handlePaymentFailed(invoice: any): Promise<void> {
452
+ const customerId = invoice.customer;
453
+
454
+ console.log(`[Billing] Payment failed for ${customerId}`);
455
+
456
+ // Could send email notification, etc.
457
+ // For now just log it
458
+ }
459
+
460
+ export default router;