mcp-twin 1.3.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.
@@ -12,6 +12,8 @@ import { initializeDatabase, closePool } from './db';
12
12
  import authRoutes from './routes/auth';
13
13
  import twinRoutes from './routes/twins';
14
14
  import usageRoutes from './routes/usage';
15
+ import billingRoutes from './routes/billing';
16
+ import { isStripeConfigured } from './stripe';
15
17
 
16
18
  const app = express();
17
19
  const PORT = parseInt(process.env.PORT || '3000');
@@ -23,6 +25,11 @@ app.use(cors({
23
25
  origin: process.env.CORS_ORIGIN || '*',
24
26
  credentials: true,
25
27
  }));
28
+
29
+ // Raw body for Stripe webhooks (must be before json parser)
30
+ app.use('/api/billing/webhook', express.raw({ type: 'application/json' }));
31
+
32
+ // JSON parser for all other routes
26
33
  app.use(express.json({ limit: '10mb' }));
27
34
 
28
35
  // Request logging
@@ -42,8 +49,9 @@ app.get('/health', (req: Request, res: Response) => {
42
49
  res.json({
43
50
  status: 'healthy',
44
51
  service: 'mcp-twin-cloud',
45
- version: '1.2.0',
52
+ version: '1.4.0',
46
53
  timestamp: new Date().toISOString(),
54
+ stripe: isStripeConfigured(),
47
55
  });
48
56
  });
49
57
 
@@ -51,6 +59,7 @@ app.get('/health', (req: Request, res: Response) => {
51
59
  app.use('/api/auth', authRoutes);
52
60
  app.use('/api/twins', twinRoutes);
53
61
  app.use('/api/usage', usageRoutes);
62
+ app.use('/api/billing', billingRoutes);
54
63
 
55
64
  // API documentation
56
65
  app.get('/api', (req: Request, res: Response) => {
@@ -86,6 +95,14 @@ app.get('/api', (req: Request, res: Response) => {
86
95
  'GET /api/usage/history': 'Get usage history',
87
96
  'GET /api/usage/by-twin': 'Get usage by twin',
88
97
  },
98
+ billing: {
99
+ 'GET /api/billing/status': 'Get billing status',
100
+ 'GET /api/billing/plans': 'Get available plans',
101
+ 'POST /api/billing/checkout': 'Create Stripe checkout session',
102
+ 'POST /api/billing/portal': 'Create billing portal session',
103
+ 'GET /api/billing/invoices': 'Get invoices',
104
+ 'POST /api/billing/webhook': 'Stripe webhook endpoint',
105
+ },
89
106
  },
90
107
  });
91
108
  });
@@ -128,11 +145,14 @@ export async function startServer(): Promise<void> {
128
145
  console.log(`[Server] Listening on http://${HOST}:${PORT}`);
129
146
  console.log('');
130
147
  console.log('API Endpoints:');
131
- console.log(` Health: http://${HOST}:${PORT}/health`);
132
- console.log(` Docs: http://${HOST}:${PORT}/api`);
133
- console.log(` Auth: http://${HOST}:${PORT}/api/auth/*`);
134
- console.log(` Twins: http://${HOST}:${PORT}/api/twins/*`);
135
- console.log(` Usage: http://${HOST}:${PORT}/api/usage/*`);
148
+ console.log(` Health: http://${HOST}:${PORT}/health`);
149
+ console.log(` Docs: http://${HOST}:${PORT}/api`);
150
+ console.log(` Auth: http://${HOST}:${PORT}/api/auth/*`);
151
+ console.log(` Twins: http://${HOST}:${PORT}/api/twins/*`);
152
+ console.log(` Usage: http://${HOST}:${PORT}/api/usage/*`);
153
+ console.log(` Billing: http://${HOST}:${PORT}/api/billing/*`);
154
+ console.log('');
155
+ console.log(`Stripe: ${isStripeConfigured() ? 'configured' : 'not configured (set STRIPE_SECRET_KEY)'}`);
136
156
  console.log('');
137
157
  console.log('─────────────────────────────────────────');
138
158
  });
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Stripe Integration
3
+ * MCP Twin Cloud
4
+ */
5
+
6
+ import Stripe from 'stripe';
7
+
8
+ // Initialize Stripe
9
+ const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
10
+
11
+ let stripe: Stripe | null = null;
12
+
13
+ export function getStripe(): Stripe {
14
+ if (!stripe) {
15
+ if (!stripeSecretKey) {
16
+ throw new Error('STRIPE_SECRET_KEY environment variable is required');
17
+ }
18
+ stripe = new Stripe(stripeSecretKey);
19
+ }
20
+ return stripe;
21
+ }
22
+
23
+ export function isStripeConfigured(): boolean {
24
+ return !!stripeSecretKey;
25
+ }
26
+
27
+ // Price IDs - Set these in your Stripe dashboard
28
+ export const PRICE_IDS = {
29
+ starter_monthly: process.env.STRIPE_PRICE_STARTER || 'price_starter_monthly',
30
+ pro_monthly: process.env.STRIPE_PRICE_PRO || 'price_pro_monthly',
31
+ };
32
+
33
+ // Tier mapping from Stripe price to our tier
34
+ export const PRICE_TO_TIER: Record<string, string> = {
35
+ [PRICE_IDS.starter_monthly]: 'starter',
36
+ [PRICE_IDS.pro_monthly]: 'pro',
37
+ };
38
+
39
+ // Tier to price mapping
40
+ export const TIER_TO_PRICE: Record<string, string> = {
41
+ starter: PRICE_IDS.starter_monthly,
42
+ pro: PRICE_IDS.pro_monthly,
43
+ };
44
+
45
+ /**
46
+ * Create or get Stripe customer for user
47
+ */
48
+ export async function getOrCreateCustomer(
49
+ userId: string,
50
+ email: string,
51
+ existingCustomerId?: string | null
52
+ ): Promise<string> {
53
+ const stripe = getStripe();
54
+
55
+ if (existingCustomerId) {
56
+ return existingCustomerId;
57
+ }
58
+
59
+ const customer = await stripe.customers.create({
60
+ email,
61
+ metadata: {
62
+ userId,
63
+ },
64
+ });
65
+
66
+ return customer.id;
67
+ }
68
+
69
+ /**
70
+ * Create a checkout session for subscription
71
+ */
72
+ export async function createCheckoutSession(
73
+ customerId: string,
74
+ priceId: string,
75
+ successUrl: string,
76
+ cancelUrl: string
77
+ ): Promise<Stripe.Checkout.Session> {
78
+ const stripe = getStripe();
79
+
80
+ const session = await stripe.checkout.sessions.create({
81
+ customer: customerId,
82
+ payment_method_types: ['card'],
83
+ line_items: [
84
+ {
85
+ price: priceId,
86
+ quantity: 1,
87
+ },
88
+ ],
89
+ mode: 'subscription',
90
+ success_url: successUrl,
91
+ cancel_url: cancelUrl,
92
+ allow_promotion_codes: true,
93
+ });
94
+
95
+ return session;
96
+ }
97
+
98
+ /**
99
+ * Create a billing portal session
100
+ */
101
+ export async function createPortalSession(
102
+ customerId: string,
103
+ returnUrl: string
104
+ ): Promise<Stripe.BillingPortal.Session> {
105
+ const stripe = getStripe();
106
+
107
+ const session = await stripe.billingPortal.sessions.create({
108
+ customer: customerId,
109
+ return_url: returnUrl,
110
+ });
111
+
112
+ return session;
113
+ }
114
+
115
+ /**
116
+ * Get subscription details
117
+ */
118
+ export async function getSubscription(
119
+ subscriptionId: string
120
+ ): Promise<Stripe.Subscription | null> {
121
+ const stripe = getStripe();
122
+
123
+ try {
124
+ const subscription = await stripe.subscriptions.retrieve(subscriptionId);
125
+ return subscription;
126
+ } catch (error) {
127
+ return null;
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Cancel subscription
133
+ */
134
+ export async function cancelSubscription(
135
+ subscriptionId: string,
136
+ immediately: boolean = false
137
+ ): Promise<Stripe.Subscription> {
138
+ const stripe = getStripe();
139
+
140
+ if (immediately) {
141
+ return stripe.subscriptions.cancel(subscriptionId);
142
+ }
143
+
144
+ // Cancel at period end
145
+ return stripe.subscriptions.update(subscriptionId, {
146
+ cancel_at_period_end: true,
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Get customer's invoices
152
+ */
153
+ export async function getInvoices(
154
+ customerId: string,
155
+ limit: number = 10
156
+ ): Promise<Stripe.Invoice[]> {
157
+ const stripe = getStripe();
158
+
159
+ const invoices = await stripe.invoices.list({
160
+ customer: customerId,
161
+ limit,
162
+ });
163
+
164
+ return invoices.data;
165
+ }
166
+
167
+ /**
168
+ * Construct webhook event from request
169
+ */
170
+ export function constructWebhookEvent(
171
+ payload: Buffer,
172
+ signature: string,
173
+ webhookSecret: string
174
+ ): Stripe.Event {
175
+ const stripe = getStripe();
176
+ return stripe.webhooks.constructEvent(payload, signature, webhookSecret);
177
+ }
178
+
179
+ export default {
180
+ getStripe,
181
+ isStripeConfigured,
182
+ getOrCreateCustomer,
183
+ createCheckoutSession,
184
+ createPortalSession,
185
+ getSubscription,
186
+ cancelSubscription,
187
+ getInvoices,
188
+ constructWebhookEvent,
189
+ PRICE_IDS,
190
+ PRICE_TO_TIER,
191
+ TIER_TO_PRICE,
192
+ };