mcp-twin 1.2.0 → 1.3.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 +24 -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/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 +143 -0
- package/dist/cloud/server.js.map +1 -0
- package/package.json +23 -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/twins.ts +908 -0
- package/src/cloud/routes/usage.ts +186 -0
- package/src/cloud/server.ts +151 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth Routes
|
|
3
|
+
* MCP Twin Cloud
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Router, Request, Response } from 'express';
|
|
7
|
+
import { query, queryOne } from '../db';
|
|
8
|
+
import {
|
|
9
|
+
hashPassword,
|
|
10
|
+
verifyPassword,
|
|
11
|
+
generateSessionToken,
|
|
12
|
+
generateApiKey,
|
|
13
|
+
authenticateApiKey,
|
|
14
|
+
User,
|
|
15
|
+
ApiKey,
|
|
16
|
+
} from '../auth';
|
|
17
|
+
|
|
18
|
+
const router = Router();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* POST /api/auth/signup
|
|
22
|
+
* Create a new account
|
|
23
|
+
*/
|
|
24
|
+
router.post('/signup', async (req: Request, res: Response) => {
|
|
25
|
+
try {
|
|
26
|
+
const { email, password } = req.body;
|
|
27
|
+
|
|
28
|
+
// Validate input
|
|
29
|
+
if (!email || !password) {
|
|
30
|
+
res.status(400).json({
|
|
31
|
+
error: {
|
|
32
|
+
code: 'VALIDATION_ERROR',
|
|
33
|
+
message: 'Email and password are required',
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (password.length < 8) {
|
|
40
|
+
res.status(400).json({
|
|
41
|
+
error: {
|
|
42
|
+
code: 'VALIDATION_ERROR',
|
|
43
|
+
message: 'Password must be at least 8 characters',
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check if user exists
|
|
50
|
+
const existing = await queryOne<User>(
|
|
51
|
+
'SELECT id FROM users WHERE email = $1',
|
|
52
|
+
[email.toLowerCase()]
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
if (existing) {
|
|
56
|
+
res.status(409).json({
|
|
57
|
+
error: {
|
|
58
|
+
code: 'USER_EXISTS',
|
|
59
|
+
message: 'An account with this email already exists',
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Create user
|
|
66
|
+
const passwordHash = await hashPassword(password);
|
|
67
|
+
const users = await query<User>(
|
|
68
|
+
`INSERT INTO users (email, password_hash)
|
|
69
|
+
VALUES ($1, $2)
|
|
70
|
+
RETURNING id, email, tier, created_at`,
|
|
71
|
+
[email.toLowerCase(), passwordHash]
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const user = users[0];
|
|
75
|
+
|
|
76
|
+
// Generate initial API key
|
|
77
|
+
const { key, hash, prefix } = await generateApiKey();
|
|
78
|
+
await query(
|
|
79
|
+
`INSERT INTO api_keys (user_id, key_hash, key_prefix, name)
|
|
80
|
+
VALUES ($1, $2, $3, $4)`,
|
|
81
|
+
[user.id, hash, prefix, 'Default API Key']
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Generate session token
|
|
85
|
+
const sessionToken = generateSessionToken({
|
|
86
|
+
id: user.id,
|
|
87
|
+
email: user.email,
|
|
88
|
+
tier: user.tier,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
res.status(201).json({
|
|
92
|
+
message: 'Account created successfully',
|
|
93
|
+
user: {
|
|
94
|
+
id: user.id,
|
|
95
|
+
email: user.email,
|
|
96
|
+
tier: user.tier,
|
|
97
|
+
},
|
|
98
|
+
apiKey: key,
|
|
99
|
+
sessionToken,
|
|
100
|
+
});
|
|
101
|
+
} catch (error: any) {
|
|
102
|
+
console.error('[Auth] Signup error:', error);
|
|
103
|
+
res.status(500).json({
|
|
104
|
+
error: {
|
|
105
|
+
code: 'INTERNAL_ERROR',
|
|
106
|
+
message: 'Failed to create account',
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* POST /api/auth/login
|
|
114
|
+
* Login and get session token
|
|
115
|
+
*/
|
|
116
|
+
router.post('/login', async (req: Request, res: Response) => {
|
|
117
|
+
try {
|
|
118
|
+
const { email, password } = req.body;
|
|
119
|
+
|
|
120
|
+
if (!email || !password) {
|
|
121
|
+
res.status(400).json({
|
|
122
|
+
error: {
|
|
123
|
+
code: 'VALIDATION_ERROR',
|
|
124
|
+
message: 'Email and password are required',
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Find user
|
|
131
|
+
const user = await queryOne<User>(
|
|
132
|
+
'SELECT * FROM users WHERE email = $1',
|
|
133
|
+
[email.toLowerCase()]
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (!user) {
|
|
137
|
+
res.status(401).json({
|
|
138
|
+
error: {
|
|
139
|
+
code: 'INVALID_CREDENTIALS',
|
|
140
|
+
message: 'Invalid email or password',
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Verify password
|
|
147
|
+
const valid = await verifyPassword(password, user.password_hash);
|
|
148
|
+
if (!valid) {
|
|
149
|
+
res.status(401).json({
|
|
150
|
+
error: {
|
|
151
|
+
code: 'INVALID_CREDENTIALS',
|
|
152
|
+
message: 'Invalid email or password',
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Generate session token
|
|
159
|
+
const sessionToken = generateSessionToken({
|
|
160
|
+
id: user.id,
|
|
161
|
+
email: user.email,
|
|
162
|
+
tier: user.tier,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
res.json({
|
|
166
|
+
message: 'Login successful',
|
|
167
|
+
user: {
|
|
168
|
+
id: user.id,
|
|
169
|
+
email: user.email,
|
|
170
|
+
tier: user.tier,
|
|
171
|
+
},
|
|
172
|
+
sessionToken,
|
|
173
|
+
expiresIn: '7d',
|
|
174
|
+
});
|
|
175
|
+
} catch (error: any) {
|
|
176
|
+
console.error('[Auth] Login error:', error);
|
|
177
|
+
res.status(500).json({
|
|
178
|
+
error: {
|
|
179
|
+
code: 'INTERNAL_ERROR',
|
|
180
|
+
message: 'Login failed',
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* GET /api/auth/me
|
|
188
|
+
* Get current user info
|
|
189
|
+
*/
|
|
190
|
+
router.get('/me', authenticateApiKey, async (req: Request, res: Response) => {
|
|
191
|
+
try {
|
|
192
|
+
const user = await queryOne<User>(
|
|
193
|
+
'SELECT id, email, tier, stripe_customer_id, created_at FROM users WHERE id = $1',
|
|
194
|
+
[req.user!.id]
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
if (!user) {
|
|
198
|
+
res.status(404).json({
|
|
199
|
+
error: {
|
|
200
|
+
code: 'NOT_FOUND',
|
|
201
|
+
message: 'User not found',
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Get API key count
|
|
208
|
+
const apiKeys = await query<{ count: string }>(
|
|
209
|
+
'SELECT COUNT(*) as count FROM api_keys WHERE user_id = $1 AND revoked_at IS NULL',
|
|
210
|
+
[user.id]
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// Get twin count
|
|
214
|
+
const twins = await query<{ count: string }>(
|
|
215
|
+
'SELECT COUNT(*) as count FROM twins WHERE user_id = $1',
|
|
216
|
+
[user.id]
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
res.json({
|
|
220
|
+
user: {
|
|
221
|
+
id: user.id,
|
|
222
|
+
email: user.email,
|
|
223
|
+
tier: user.tier,
|
|
224
|
+
createdAt: user.created_at,
|
|
225
|
+
},
|
|
226
|
+
stats: {
|
|
227
|
+
apiKeys: parseInt(apiKeys[0]?.count || '0'),
|
|
228
|
+
twins: parseInt(twins[0]?.count || '0'),
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
} catch (error: any) {
|
|
232
|
+
console.error('[Auth] Get me error:', error);
|
|
233
|
+
res.status(500).json({
|
|
234
|
+
error: {
|
|
235
|
+
code: 'INTERNAL_ERROR',
|
|
236
|
+
message: 'Failed to get user info',
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* POST /api/auth/api-keys
|
|
244
|
+
* Create a new API key
|
|
245
|
+
*/
|
|
246
|
+
router.post('/api-keys', authenticateApiKey, async (req: Request, res: Response) => {
|
|
247
|
+
try {
|
|
248
|
+
const { name } = req.body;
|
|
249
|
+
|
|
250
|
+
// Generate new key
|
|
251
|
+
const { key, hash, prefix } = await generateApiKey();
|
|
252
|
+
|
|
253
|
+
const keys = await query<ApiKey>(
|
|
254
|
+
`INSERT INTO api_keys (user_id, key_hash, key_prefix, name)
|
|
255
|
+
VALUES ($1, $2, $3, $4)
|
|
256
|
+
RETURNING id, key_prefix, name, created_at`,
|
|
257
|
+
[req.user!.id, hash, prefix, name || 'API Key']
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
res.status(201).json({
|
|
261
|
+
message: 'API key created',
|
|
262
|
+
apiKey: {
|
|
263
|
+
id: keys[0].id,
|
|
264
|
+
key, // Only returned once at creation
|
|
265
|
+
prefix: keys[0].key_prefix,
|
|
266
|
+
name: keys[0].name,
|
|
267
|
+
createdAt: keys[0].created_at,
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
} catch (error: any) {
|
|
271
|
+
console.error('[Auth] Create API key error:', error);
|
|
272
|
+
res.status(500).json({
|
|
273
|
+
error: {
|
|
274
|
+
code: 'INTERNAL_ERROR',
|
|
275
|
+
message: 'Failed to create API key',
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* GET /api/auth/api-keys
|
|
283
|
+
* List user's API keys
|
|
284
|
+
*/
|
|
285
|
+
router.get('/api-keys', authenticateApiKey, async (req: Request, res: Response) => {
|
|
286
|
+
try {
|
|
287
|
+
const keys = await query<ApiKey>(
|
|
288
|
+
`SELECT id, key_prefix, name, last_used_at, created_at
|
|
289
|
+
FROM api_keys
|
|
290
|
+
WHERE user_id = $1 AND revoked_at IS NULL
|
|
291
|
+
ORDER BY created_at DESC`,
|
|
292
|
+
[req.user!.id]
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
res.json({
|
|
296
|
+
apiKeys: keys.map((k) => ({
|
|
297
|
+
id: k.id,
|
|
298
|
+
prefix: k.key_prefix,
|
|
299
|
+
name: k.name,
|
|
300
|
+
lastUsedAt: k.last_used_at,
|
|
301
|
+
createdAt: k.created_at,
|
|
302
|
+
})),
|
|
303
|
+
});
|
|
304
|
+
} catch (error: any) {
|
|
305
|
+
console.error('[Auth] List API keys error:', error);
|
|
306
|
+
res.status(500).json({
|
|
307
|
+
error: {
|
|
308
|
+
code: 'INTERNAL_ERROR',
|
|
309
|
+
message: 'Failed to list API keys',
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* DELETE /api/auth/api-keys/:id
|
|
317
|
+
* Revoke an API key
|
|
318
|
+
*/
|
|
319
|
+
router.delete('/api-keys/:id', authenticateApiKey, async (req: Request, res: Response) => {
|
|
320
|
+
try {
|
|
321
|
+
const { id } = req.params;
|
|
322
|
+
|
|
323
|
+
const result = await query(
|
|
324
|
+
`UPDATE api_keys
|
|
325
|
+
SET revoked_at = NOW()
|
|
326
|
+
WHERE id = $1 AND user_id = $2 AND revoked_at IS NULL
|
|
327
|
+
RETURNING id`,
|
|
328
|
+
[id, req.user!.id]
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
if (result.length === 0) {
|
|
332
|
+
res.status(404).json({
|
|
333
|
+
error: {
|
|
334
|
+
code: 'NOT_FOUND',
|
|
335
|
+
message: 'API key not found',
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
res.json({
|
|
342
|
+
message: 'API key revoked',
|
|
343
|
+
});
|
|
344
|
+
} catch (error: any) {
|
|
345
|
+
console.error('[Auth] Revoke API key error:', error);
|
|
346
|
+
res.status(500).json({
|
|
347
|
+
error: {
|
|
348
|
+
code: 'INTERNAL_ERROR',
|
|
349
|
+
message: 'Failed to revoke API key',
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
export default router;
|