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.
- package/.env.example +30 -0
- package/PRD.md +682 -0
- package/dist/cli.js +41 -0
- package/dist/cli.js.map +1 -1
- package/dist/cloud/auth.d.ts +108 -0
- package/dist/cloud/auth.d.ts.map +1 -0
- package/dist/cloud/auth.js +199 -0
- package/dist/cloud/auth.js.map +1 -0
- package/dist/cloud/db.d.ts +21 -0
- package/dist/cloud/db.d.ts.map +1 -0
- package/dist/cloud/db.js +158 -0
- package/dist/cloud/db.js.map +1 -0
- package/dist/cloud/routes/auth.d.ts +7 -0
- package/dist/cloud/routes/auth.d.ts.map +1 -0
- package/dist/cloud/routes/auth.js +291 -0
- package/dist/cloud/routes/auth.js.map +1 -0
- package/dist/cloud/routes/billing.d.ts +7 -0
- package/dist/cloud/routes/billing.d.ts.map +1 -0
- package/dist/cloud/routes/billing.js +368 -0
- package/dist/cloud/routes/billing.js.map +1 -0
- package/dist/cloud/routes/twins.d.ts +7 -0
- package/dist/cloud/routes/twins.d.ts.map +1 -0
- package/dist/cloud/routes/twins.js +740 -0
- package/dist/cloud/routes/twins.js.map +1 -0
- package/dist/cloud/routes/usage.d.ts +7 -0
- package/dist/cloud/routes/usage.d.ts.map +1 -0
- package/dist/cloud/routes/usage.js +145 -0
- package/dist/cloud/routes/usage.js.map +1 -0
- package/dist/cloud/server.d.ts +10 -0
- package/dist/cloud/server.d.ts.map +1 -0
- package/dist/cloud/server.js +161 -0
- package/dist/cloud/server.js.map +1 -0
- package/dist/cloud/stripe.d.ts +60 -0
- package/dist/cloud/stripe.d.ts.map +1 -0
- package/dist/cloud/stripe.js +157 -0
- package/dist/cloud/stripe.js.map +1 -0
- package/package.json +25 -4
- package/src/cli.ts +10 -0
- package/src/cloud/auth.ts +269 -0
- package/src/cloud/db.ts +167 -0
- package/src/cloud/routes/auth.ts +355 -0
- package/src/cloud/routes/billing.ts +460 -0
- package/src/cloud/routes/twins.ts +908 -0
- package/src/cloud/routes/usage.ts +186 -0
- package/src/cloud/server.ts +171 -0
- package/src/cloud/stripe.ts +192 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage Routes
|
|
3
|
+
* MCP Twin Cloud
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Router, Request, Response } from 'express';
|
|
7
|
+
import { query, queryOne } from '../db';
|
|
8
|
+
import { authenticateApiKey, getTierLimits } from '../auth';
|
|
9
|
+
|
|
10
|
+
const router = Router();
|
|
11
|
+
|
|
12
|
+
interface UsageDaily {
|
|
13
|
+
id: string;
|
|
14
|
+
user_id: string;
|
|
15
|
+
date: string;
|
|
16
|
+
request_count: number;
|
|
17
|
+
error_count: number;
|
|
18
|
+
total_duration_ms: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* GET /api/usage
|
|
23
|
+
* Get current month usage
|
|
24
|
+
*/
|
|
25
|
+
router.get('/', authenticateApiKey, async (req: Request, res: Response) => {
|
|
26
|
+
try {
|
|
27
|
+
const user = req.user!;
|
|
28
|
+
const limits = getTierLimits(user.tier);
|
|
29
|
+
|
|
30
|
+
// Get current month start
|
|
31
|
+
const now = new Date();
|
|
32
|
+
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
33
|
+
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
|
34
|
+
|
|
35
|
+
// Get aggregated usage for current month
|
|
36
|
+
const usage = await queryOne<{
|
|
37
|
+
total_requests: string;
|
|
38
|
+
total_errors: string;
|
|
39
|
+
total_duration: string;
|
|
40
|
+
}>(
|
|
41
|
+
`SELECT
|
|
42
|
+
COALESCE(SUM(request_count), 0) as total_requests,
|
|
43
|
+
COALESCE(SUM(error_count), 0) as total_errors,
|
|
44
|
+
COALESCE(SUM(total_duration_ms), 0) as total_duration
|
|
45
|
+
FROM usage_daily
|
|
46
|
+
WHERE user_id = $1 AND date >= $2 AND date <= $3`,
|
|
47
|
+
[user.id, startOfMonth.toISOString().split('T')[0], endOfMonth.toISOString().split('T')[0]]
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
// Get twin count
|
|
51
|
+
const twinCount = await queryOne<{ count: string }>(
|
|
52
|
+
'SELECT COUNT(*) as count FROM twins WHERE user_id = $1',
|
|
53
|
+
[user.id]
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const totalRequests = parseInt(usage?.total_requests || '0');
|
|
57
|
+
const totalErrors = parseInt(usage?.total_errors || '0');
|
|
58
|
+
const twins = parseInt(twinCount?.count || '0');
|
|
59
|
+
|
|
60
|
+
res.json({
|
|
61
|
+
tier: user.tier,
|
|
62
|
+
period: {
|
|
63
|
+
start: startOfMonth.toISOString().split('T')[0],
|
|
64
|
+
end: endOfMonth.toISOString().split('T')[0],
|
|
65
|
+
},
|
|
66
|
+
twins: {
|
|
67
|
+
used: twins,
|
|
68
|
+
limit: limits.twins === -1 ? 'unlimited' : limits.twins,
|
|
69
|
+
},
|
|
70
|
+
requests: {
|
|
71
|
+
used: totalRequests,
|
|
72
|
+
limit: limits.requestsPerMonth === -1 ? 'unlimited' : limits.requestsPerMonth,
|
|
73
|
+
percentUsed: limits.requestsPerMonth === -1 ? 0 : Math.round((totalRequests / limits.requestsPerMonth) * 100),
|
|
74
|
+
},
|
|
75
|
+
errors: {
|
|
76
|
+
count: totalErrors,
|
|
77
|
+
rate: totalRequests > 0 ? Math.round((totalErrors / totalRequests) * 100) : 0,
|
|
78
|
+
},
|
|
79
|
+
avgLatencyMs: totalRequests > 0
|
|
80
|
+
? Math.round(parseInt(usage?.total_duration || '0') / totalRequests)
|
|
81
|
+
: 0,
|
|
82
|
+
});
|
|
83
|
+
} catch (error: any) {
|
|
84
|
+
console.error('[Usage] Get error:', error);
|
|
85
|
+
res.status(500).json({
|
|
86
|
+
error: {
|
|
87
|
+
code: 'INTERNAL_ERROR',
|
|
88
|
+
message: 'Failed to get usage',
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* GET /api/usage/history
|
|
96
|
+
* Get usage history (daily breakdown)
|
|
97
|
+
*/
|
|
98
|
+
router.get('/history', authenticateApiKey, async (req: Request, res: Response) => {
|
|
99
|
+
try {
|
|
100
|
+
const { days = '30' } = req.query;
|
|
101
|
+
const daysNum = Math.min(parseInt(days as string) || 30, 90);
|
|
102
|
+
|
|
103
|
+
const startDate = new Date();
|
|
104
|
+
startDate.setDate(startDate.getDate() - daysNum);
|
|
105
|
+
|
|
106
|
+
const usage = await query<UsageDaily>(
|
|
107
|
+
`SELECT date, request_count, error_count, total_duration_ms
|
|
108
|
+
FROM usage_daily
|
|
109
|
+
WHERE user_id = $1 AND date >= $2
|
|
110
|
+
ORDER BY date DESC`,
|
|
111
|
+
[req.user!.id, startDate.toISOString().split('T')[0]]
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
res.json({
|
|
115
|
+
history: usage.map((u) => ({
|
|
116
|
+
date: u.date,
|
|
117
|
+
requests: u.request_count,
|
|
118
|
+
errors: u.error_count,
|
|
119
|
+
avgLatencyMs: u.request_count > 0
|
|
120
|
+
? Math.round(u.total_duration_ms / u.request_count)
|
|
121
|
+
: 0,
|
|
122
|
+
})),
|
|
123
|
+
});
|
|
124
|
+
} catch (error: any) {
|
|
125
|
+
console.error('[Usage] Get history error:', error);
|
|
126
|
+
res.status(500).json({
|
|
127
|
+
error: {
|
|
128
|
+
code: 'INTERNAL_ERROR',
|
|
129
|
+
message: 'Failed to get usage history',
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* GET /api/usage/by-twin
|
|
137
|
+
* Get usage breakdown by twin
|
|
138
|
+
*/
|
|
139
|
+
router.get('/by-twin', authenticateApiKey, async (req: Request, res: Response) => {
|
|
140
|
+
try {
|
|
141
|
+
// Get current month
|
|
142
|
+
const now = new Date();
|
|
143
|
+
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
144
|
+
|
|
145
|
+
const usage = await query<{
|
|
146
|
+
twin_id: string;
|
|
147
|
+
twin_name: string;
|
|
148
|
+
request_count: string;
|
|
149
|
+
error_count: string;
|
|
150
|
+
avg_duration: string;
|
|
151
|
+
}>(
|
|
152
|
+
`SELECT
|
|
153
|
+
t.id as twin_id,
|
|
154
|
+
t.name as twin_name,
|
|
155
|
+
COUNT(l.id) as request_count,
|
|
156
|
+
SUM(CASE WHEN l.status = 'error' THEN 1 ELSE 0 END) as error_count,
|
|
157
|
+
AVG(l.duration_ms) as avg_duration
|
|
158
|
+
FROM twins t
|
|
159
|
+
LEFT JOIN logs l ON l.twin_id = t.id AND l.created_at >= $2
|
|
160
|
+
WHERE t.user_id = $1
|
|
161
|
+
GROUP BY t.id, t.name
|
|
162
|
+
ORDER BY request_count DESC`,
|
|
163
|
+
[req.user!.id, startOfMonth.toISOString()]
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
res.json({
|
|
167
|
+
byTwin: usage.map((u) => ({
|
|
168
|
+
twinId: u.twin_id,
|
|
169
|
+
twinName: u.twin_name,
|
|
170
|
+
requests: parseInt(u.request_count || '0'),
|
|
171
|
+
errors: parseInt(u.error_count || '0'),
|
|
172
|
+
avgLatencyMs: Math.round(parseFloat(u.avg_duration || '0')),
|
|
173
|
+
})),
|
|
174
|
+
});
|
|
175
|
+
} catch (error: any) {
|
|
176
|
+
console.error('[Usage] Get by twin error:', error);
|
|
177
|
+
res.status(500).json({
|
|
178
|
+
error: {
|
|
179
|
+
code: 'INTERNAL_ERROR',
|
|
180
|
+
message: 'Failed to get usage by twin',
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
export default router;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Twin Cloud Server
|
|
3
|
+
* Zero-downtime MCP server management as a service
|
|
4
|
+
*
|
|
5
|
+
* Powered by Prax Chat - https://prax.chat
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import express, { Request, Response, NextFunction } from 'express';
|
|
9
|
+
import cors from 'cors';
|
|
10
|
+
import helmet from 'helmet';
|
|
11
|
+
import { initializeDatabase, closePool } from './db';
|
|
12
|
+
import authRoutes from './routes/auth';
|
|
13
|
+
import twinRoutes from './routes/twins';
|
|
14
|
+
import usageRoutes from './routes/usage';
|
|
15
|
+
import billingRoutes from './routes/billing';
|
|
16
|
+
import { isStripeConfigured } from './stripe';
|
|
17
|
+
|
|
18
|
+
const app = express();
|
|
19
|
+
const PORT = parseInt(process.env.PORT || '3000');
|
|
20
|
+
const HOST = process.env.HOST || '0.0.0.0';
|
|
21
|
+
|
|
22
|
+
// Middleware
|
|
23
|
+
app.use(helmet());
|
|
24
|
+
app.use(cors({
|
|
25
|
+
origin: process.env.CORS_ORIGIN || '*',
|
|
26
|
+
credentials: true,
|
|
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
|
|
33
|
+
app.use(express.json({ limit: '10mb' }));
|
|
34
|
+
|
|
35
|
+
// Request logging
|
|
36
|
+
app.use((req: Request, res: Response, next: NextFunction) => {
|
|
37
|
+
const start = Date.now();
|
|
38
|
+
res.on('finish', () => {
|
|
39
|
+
const duration = Date.now() - start;
|
|
40
|
+
if (process.env.DEBUG_HTTP) {
|
|
41
|
+
console.log(`[HTTP] ${req.method} ${req.path} ${res.statusCode} ${duration}ms`);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
next();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Health check
|
|
48
|
+
app.get('/health', (req: Request, res: Response) => {
|
|
49
|
+
res.json({
|
|
50
|
+
status: 'healthy',
|
|
51
|
+
service: 'mcp-twin-cloud',
|
|
52
|
+
version: '1.4.0',
|
|
53
|
+
timestamp: new Date().toISOString(),
|
|
54
|
+
stripe: isStripeConfigured(),
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// API Routes
|
|
59
|
+
app.use('/api/auth', authRoutes);
|
|
60
|
+
app.use('/api/twins', twinRoutes);
|
|
61
|
+
app.use('/api/usage', usageRoutes);
|
|
62
|
+
app.use('/api/billing', billingRoutes);
|
|
63
|
+
|
|
64
|
+
// API documentation
|
|
65
|
+
app.get('/api', (req: Request, res: Response) => {
|
|
66
|
+
res.json({
|
|
67
|
+
name: 'MCP Twin Cloud API',
|
|
68
|
+
version: '1.0.0',
|
|
69
|
+
documentation: 'https://prax.chat/mcp-twin/docs',
|
|
70
|
+
endpoints: {
|
|
71
|
+
auth: {
|
|
72
|
+
'POST /api/auth/signup': 'Create account',
|
|
73
|
+
'POST /api/auth/login': 'Login and get session token',
|
|
74
|
+
'GET /api/auth/me': 'Get current user info',
|
|
75
|
+
'POST /api/auth/api-keys': 'Create API key',
|
|
76
|
+
'GET /api/auth/api-keys': 'List API keys',
|
|
77
|
+
'DELETE /api/auth/api-keys/:id': 'Revoke API key',
|
|
78
|
+
},
|
|
79
|
+
twins: {
|
|
80
|
+
'POST /api/twins': 'Create twin',
|
|
81
|
+
'GET /api/twins': 'List twins',
|
|
82
|
+
'GET /api/twins/:id': 'Get twin details',
|
|
83
|
+
'PATCH /api/twins/:id': 'Update twin',
|
|
84
|
+
'DELETE /api/twins/:id': 'Delete twin',
|
|
85
|
+
'POST /api/twins/:id/start': 'Start twin',
|
|
86
|
+
'POST /api/twins/:id/stop': 'Stop twin',
|
|
87
|
+
'POST /api/twins/:id/reload': 'Reload standby',
|
|
88
|
+
'POST /api/twins/:id/swap': 'Swap active server',
|
|
89
|
+
'POST /api/twins/:id/call': 'Execute tool call',
|
|
90
|
+
'GET /api/twins/:id/logs': 'Get execution logs',
|
|
91
|
+
'GET /api/twins/:id/status': 'Get health status',
|
|
92
|
+
},
|
|
93
|
+
usage: {
|
|
94
|
+
'GET /api/usage': 'Get current month usage',
|
|
95
|
+
'GET /api/usage/history': 'Get usage history',
|
|
96
|
+
'GET /api/usage/by-twin': 'Get usage by twin',
|
|
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
|
+
},
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// 404 handler
|
|
111
|
+
app.use((req: Request, res: Response) => {
|
|
112
|
+
res.status(404).json({
|
|
113
|
+
error: {
|
|
114
|
+
code: 'NOT_FOUND',
|
|
115
|
+
message: `Route ${req.method} ${req.path} not found`,
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Error handler
|
|
121
|
+
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
|
|
122
|
+
console.error('[Server] Unhandled error:', err);
|
|
123
|
+
res.status(500).json({
|
|
124
|
+
error: {
|
|
125
|
+
code: 'INTERNAL_ERROR',
|
|
126
|
+
message: 'An unexpected error occurred',
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Start server
|
|
132
|
+
export async function startServer(): Promise<void> {
|
|
133
|
+
console.log('─────────────────────────────────────────');
|
|
134
|
+
console.log('⚡ MCP Twin Cloud');
|
|
135
|
+
console.log(' Powered by Prax Chat');
|
|
136
|
+
console.log('─────────────────────────────────────────');
|
|
137
|
+
console.log('');
|
|
138
|
+
|
|
139
|
+
// Initialize database
|
|
140
|
+
await initializeDatabase();
|
|
141
|
+
console.log('[Server] Database initialized');
|
|
142
|
+
|
|
143
|
+
// Start listening
|
|
144
|
+
app.listen(PORT, HOST, () => {
|
|
145
|
+
console.log(`[Server] Listening on http://${HOST}:${PORT}`);
|
|
146
|
+
console.log('');
|
|
147
|
+
console.log('API Endpoints:');
|
|
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)'}`);
|
|
156
|
+
console.log('');
|
|
157
|
+
console.log('─────────────────────────────────────────');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Graceful shutdown
|
|
161
|
+
const shutdown = async () => {
|
|
162
|
+
console.log('\n[Server] Shutting down...');
|
|
163
|
+
await closePool();
|
|
164
|
+
process.exit(0);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
process.on('SIGINT', shutdown);
|
|
168
|
+
process.on('SIGTERM', shutdown);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export default app;
|
|
@@ -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
|
+
};
|