@nordsym/apiclaw 1.3.3 → 1.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/convex/_generated/api.d.ts +6 -0
- package/convex/billing.ts +341 -0
- package/convex/email.ts +276 -0
- package/convex/http.ts +154 -0
- package/convex/schema.ts +43 -0
- package/convex/workspaces.ts +663 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +272 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.js +396 -4
- package/dist/index.js.map +1 -1
- package/dist/session.d.ts +29 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +87 -0
- package/dist/session.js.map +1 -0
- package/docs/PRD-agent-first-billing.md +525 -0
- package/docs/PRD-workspace-fixes.md +178 -0
- package/landing/package-lock.json +21 -3
- package/landing/package.json +2 -1
- package/landing/src/app/api/stripe/webhook/route.ts +178 -0
- package/landing/src/app/api/workspace-auth/magic-link/route.ts +84 -0
- package/landing/src/app/api/workspace-auth/session/route.ts +73 -0
- package/landing/src/app/api/workspace-auth/verify/route.ts +57 -0
- package/landing/src/app/auth/verify/page.tsx +292 -0
- package/landing/src/app/dashboard/layout.tsx +22 -0
- package/landing/src/app/dashboard/page.tsx +22 -0
- package/landing/src/app/dashboard/verify/page.tsx +108 -0
- package/landing/src/app/login/page.tsx +204 -0
- package/landing/src/app/page.tsx +23 -7
- package/landing/src/app/providers/dashboard/layout.tsx +5 -4
- package/landing/src/app/providers/dashboard/page.tsx +11 -641
- package/landing/src/app/upgrade/page.tsx +288 -0
- package/landing/src/app/workspace/layout.tsx +30 -0
- package/landing/src/app/workspace/page.tsx +1637 -0
- package/landing/src/lib/stats.json +14 -15
- package/landing/src/middleware.ts +50 -0
- package/landing/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/cli.ts +320 -0
- package/src/index.ts +444 -4
- package/src/session.ts +103 -0
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
import { mutation, query } from "./_generated/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
|
|
4
|
+
// ============================================
|
|
5
|
+
// MAGIC LINK AUTH FOR WORKSPACES
|
|
6
|
+
// ============================================
|
|
7
|
+
|
|
8
|
+
// Create magic link for workspace email auth
|
|
9
|
+
export const createMagicLink = mutation({
|
|
10
|
+
args: {
|
|
11
|
+
email: v.string(),
|
|
12
|
+
fingerprint: v.optional(v.string()),
|
|
13
|
+
},
|
|
14
|
+
handler: async (ctx, { email, fingerprint }) => {
|
|
15
|
+
const token = generateToken();
|
|
16
|
+
const expiresAt = Date.now() + 15 * 60 * 1000; // 15 minutes
|
|
17
|
+
|
|
18
|
+
await ctx.db.insert("workspaceMagicLinks", {
|
|
19
|
+
email: email.toLowerCase(),
|
|
20
|
+
token,
|
|
21
|
+
sessionFingerprint: fingerprint,
|
|
22
|
+
expiresAt,
|
|
23
|
+
createdAt: Date.now(),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return { token, expiresAt };
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Verify magic link and create workspace + session
|
|
31
|
+
export const verifyMagicLink = mutation({
|
|
32
|
+
args: {
|
|
33
|
+
token: v.string(),
|
|
34
|
+
fingerprint: v.optional(v.string()),
|
|
35
|
+
},
|
|
36
|
+
handler: async (ctx, { token, fingerprint }) => {
|
|
37
|
+
const magicLink = await ctx.db
|
|
38
|
+
.query("workspaceMagicLinks")
|
|
39
|
+
.withIndex("by_token", (q) => q.eq("token", token))
|
|
40
|
+
.first();
|
|
41
|
+
|
|
42
|
+
if (!magicLink) {
|
|
43
|
+
return { success: false, error: "Invalid token" };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (magicLink.expiresAt < Date.now()) {
|
|
47
|
+
return { success: false, error: "Token expired" };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (magicLink.usedAt) {
|
|
51
|
+
return { success: false, error: "Token already used" };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Mark as used
|
|
55
|
+
await ctx.db.patch(magicLink._id, { usedAt: Date.now() });
|
|
56
|
+
|
|
57
|
+
// Find or create workspace
|
|
58
|
+
let workspace = await ctx.db
|
|
59
|
+
.query("workspaces")
|
|
60
|
+
.withIndex("by_email", (q) => q.eq("email", magicLink.email))
|
|
61
|
+
.first();
|
|
62
|
+
|
|
63
|
+
if (!workspace) {
|
|
64
|
+
// Create new workspace with free tier
|
|
65
|
+
const workspaceId = await ctx.db.insert("workspaces", {
|
|
66
|
+
email: magicLink.email,
|
|
67
|
+
status: "active",
|
|
68
|
+
tier: "free",
|
|
69
|
+
usageCount: 0,
|
|
70
|
+
usageLimit: 1000, // 1000 free API calls
|
|
71
|
+
createdAt: Date.now(),
|
|
72
|
+
updatedAt: Date.now(),
|
|
73
|
+
});
|
|
74
|
+
workspace = await ctx.db.get(workspaceId);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Create agent session
|
|
78
|
+
const sessionToken = generateToken();
|
|
79
|
+
|
|
80
|
+
await ctx.db.insert("agentSessions", {
|
|
81
|
+
workspaceId: workspace!._id,
|
|
82
|
+
sessionToken,
|
|
83
|
+
fingerprint: fingerprint || magicLink.sessionFingerprint,
|
|
84
|
+
lastUsedAt: Date.now(),
|
|
85
|
+
createdAt: Date.now(),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
success: true,
|
|
90
|
+
sessionToken,
|
|
91
|
+
workspace: {
|
|
92
|
+
id: workspace!._id,
|
|
93
|
+
email: workspace!.email,
|
|
94
|
+
tier: workspace!.tier,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Get session from token
|
|
101
|
+
export const getSession = query({
|
|
102
|
+
args: { token: v.string() },
|
|
103
|
+
handler: async (ctx, { token }) => {
|
|
104
|
+
const session = await ctx.db
|
|
105
|
+
.query("agentSessions")
|
|
106
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
|
|
107
|
+
.first();
|
|
108
|
+
|
|
109
|
+
if (!session) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const workspace = await ctx.db.get(session.workspaceId);
|
|
114
|
+
if (!workspace) return null;
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
workspaceId: workspace._id,
|
|
118
|
+
email: workspace.email,
|
|
119
|
+
tier: workspace.tier,
|
|
120
|
+
status: workspace.status,
|
|
121
|
+
usageCount: workspace.usageCount,
|
|
122
|
+
usageLimit: workspace.usageLimit,
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// ============================================
|
|
128
|
+
// DASHBOARD QUERIES
|
|
129
|
+
// ============================================
|
|
130
|
+
|
|
131
|
+
// Get full workspace dashboard data
|
|
132
|
+
export const getWorkspaceDashboard = query({
|
|
133
|
+
args: { token: v.string() },
|
|
134
|
+
handler: async (ctx, { token }) => {
|
|
135
|
+
// Verify session
|
|
136
|
+
const session = await ctx.db
|
|
137
|
+
.query("agentSessions")
|
|
138
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
|
|
139
|
+
.first();
|
|
140
|
+
|
|
141
|
+
if (!session) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Note: lastUsedAt is updated via touchSession mutation separately
|
|
146
|
+
|
|
147
|
+
const workspace = await ctx.db.get(session.workspaceId);
|
|
148
|
+
if (!workspace) return null;
|
|
149
|
+
|
|
150
|
+
// Get all agent sessions for this workspace
|
|
151
|
+
const agentSessions = await ctx.db
|
|
152
|
+
.query("agentSessions")
|
|
153
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", session.workspaceId))
|
|
154
|
+
.collect();
|
|
155
|
+
|
|
156
|
+
// Get usage logs for this workspace (via agent credits or purchases)
|
|
157
|
+
const credits = await ctx.db
|
|
158
|
+
.query("agentCredits")
|
|
159
|
+
.collect();
|
|
160
|
+
|
|
161
|
+
// Filter credits that belong to this workspace's agents
|
|
162
|
+
const workspaceCredits = credits.filter(c =>
|
|
163
|
+
agentSessions.some(s => c.agentId === s.sessionToken)
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// Get purchases for workspace agents
|
|
167
|
+
const purchases = await ctx.db
|
|
168
|
+
.query("purchases")
|
|
169
|
+
.collect();
|
|
170
|
+
|
|
171
|
+
const workspacePurchases = purchases.filter(p =>
|
|
172
|
+
agentSessions.some(s => p.agentId === s.sessionToken)
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Calculate usage remaining
|
|
176
|
+
const usageRemaining = workspace.usageLimit - workspace.usageCount;
|
|
177
|
+
const usagePercentage = (workspace.usageCount / workspace.usageLimit) * 100;
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
workspace: {
|
|
181
|
+
id: workspace._id,
|
|
182
|
+
email: workspace.email,
|
|
183
|
+
tier: workspace.tier,
|
|
184
|
+
status: workspace.status,
|
|
185
|
+
usageCount: workspace.usageCount,
|
|
186
|
+
usageLimit: workspace.usageLimit,
|
|
187
|
+
usageRemaining,
|
|
188
|
+
usagePercentage,
|
|
189
|
+
stripeCustomerId: workspace.stripeCustomerId,
|
|
190
|
+
createdAt: workspace.createdAt,
|
|
191
|
+
},
|
|
192
|
+
stats: {
|
|
193
|
+
totalAgents: agentSessions.length,
|
|
194
|
+
totalCredits: workspaceCredits.reduce((sum, c) => sum + c.balanceUsd, 0),
|
|
195
|
+
totalPurchases: workspacePurchases.length,
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Get connected agents for workspace
|
|
202
|
+
export const getConnectedAgents = query({
|
|
203
|
+
args: { token: v.string() },
|
|
204
|
+
handler: async (ctx, { token }) => {
|
|
205
|
+
const session = await ctx.db
|
|
206
|
+
.query("agentSessions")
|
|
207
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
|
|
208
|
+
.first();
|
|
209
|
+
|
|
210
|
+
if (!session) {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const agentSessions = await ctx.db
|
|
215
|
+
.query("agentSessions")
|
|
216
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", session.workspaceId))
|
|
217
|
+
.collect();
|
|
218
|
+
|
|
219
|
+
return agentSessions.map((s) => ({
|
|
220
|
+
id: s._id,
|
|
221
|
+
fingerprint: s.fingerprint || "Unknown",
|
|
222
|
+
lastUsedAt: s.lastUsedAt,
|
|
223
|
+
createdAt: s.createdAt,
|
|
224
|
+
isCurrent: s.sessionToken === token,
|
|
225
|
+
}));
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Get usage breakdown by provider
|
|
230
|
+
export const getUsageBreakdown = query({
|
|
231
|
+
args: { token: v.string() },
|
|
232
|
+
handler: async (ctx, { token }) => {
|
|
233
|
+
const session = await ctx.db
|
|
234
|
+
.query("agentSessions")
|
|
235
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
|
|
236
|
+
.first();
|
|
237
|
+
|
|
238
|
+
if (!session) {
|
|
239
|
+
return { byProvider: [], byDay: [], total: 0 };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Get all sessions for this workspace
|
|
243
|
+
const agentSessions = await ctx.db
|
|
244
|
+
.query("agentSessions")
|
|
245
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", session.workspaceId))
|
|
246
|
+
.collect();
|
|
247
|
+
|
|
248
|
+
const sessionTokens = agentSessions.map(s => s.sessionToken);
|
|
249
|
+
|
|
250
|
+
// Get purchases for these agents
|
|
251
|
+
const allPurchases = await ctx.db.query("purchases").collect();
|
|
252
|
+
const workspacePurchases = allPurchases.filter(p => sessionTokens.includes(p.agentId));
|
|
253
|
+
|
|
254
|
+
// Get usage for purchases
|
|
255
|
+
const allUsage = await ctx.db.query("usage").collect();
|
|
256
|
+
const purchaseIds = workspacePurchases.map(p => p._id);
|
|
257
|
+
const workspaceUsage = allUsage.filter(u => purchaseIds.includes(u.purchaseId));
|
|
258
|
+
|
|
259
|
+
// Aggregate by provider
|
|
260
|
+
const byProvider: Record<string, { calls: number; cost: number }> = {};
|
|
261
|
+
for (const usage of workspaceUsage) {
|
|
262
|
+
if (!byProvider[usage.providerId]) {
|
|
263
|
+
byProvider[usage.providerId] = { calls: 0, cost: 0 };
|
|
264
|
+
}
|
|
265
|
+
byProvider[usage.providerId].calls += usage.unitsUsed;
|
|
266
|
+
byProvider[usage.providerId].cost += usage.costIncurredUsd;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Aggregate by day (last 14 days)
|
|
270
|
+
const now = Date.now();
|
|
271
|
+
const fourteenDaysAgo = now - 14 * 24 * 60 * 60 * 1000;
|
|
272
|
+
const byDay: Record<string, number> = {};
|
|
273
|
+
|
|
274
|
+
for (const usage of workspaceUsage) {
|
|
275
|
+
if (usage.lastUsedAt >= fourteenDaysAgo) {
|
|
276
|
+
const day = new Date(usage.lastUsedAt).toISOString().split("T")[0];
|
|
277
|
+
byDay[day] = (byDay[day] || 0) + usage.unitsUsed;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
byProvider: Object.entries(byProvider).map(([provider, data]) => ({
|
|
283
|
+
provider,
|
|
284
|
+
calls: data.calls,
|
|
285
|
+
cost: data.cost,
|
|
286
|
+
})),
|
|
287
|
+
byDay: Object.entries(byDay)
|
|
288
|
+
.map(([date, calls]) => ({ date, calls }))
|
|
289
|
+
.sort((a, b) => a.date.localeCompare(b.date)),
|
|
290
|
+
total: workspaceUsage.reduce((sum, u) => sum + u.unitsUsed, 0),
|
|
291
|
+
};
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// ============================================
|
|
296
|
+
// AGENT MANAGEMENT
|
|
297
|
+
// ============================================
|
|
298
|
+
|
|
299
|
+
// Revoke an agent session
|
|
300
|
+
export const revokeAgentSession = mutation({
|
|
301
|
+
args: {
|
|
302
|
+
token: v.string(),
|
|
303
|
+
sessionId: v.id("agentSessions"),
|
|
304
|
+
},
|
|
305
|
+
handler: async (ctx, { token, sessionId }) => {
|
|
306
|
+
// Verify the requesting session
|
|
307
|
+
const session = await ctx.db
|
|
308
|
+
.query("agentSessions")
|
|
309
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
|
|
310
|
+
.first();
|
|
311
|
+
|
|
312
|
+
if (!session) {
|
|
313
|
+
throw new Error("Unauthorized");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Get the session to revoke
|
|
317
|
+
const targetSession = await ctx.db.get(sessionId);
|
|
318
|
+
if (!targetSession) {
|
|
319
|
+
throw new Error("Session not found");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Verify same workspace
|
|
323
|
+
if (targetSession.workspaceId !== session.workspaceId) {
|
|
324
|
+
throw new Error("Unauthorized");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Prevent revoking current session
|
|
328
|
+
if (targetSession.sessionToken === token) {
|
|
329
|
+
throw new Error("Cannot revoke current session");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Delete the session
|
|
333
|
+
await ctx.db.delete(sessionId);
|
|
334
|
+
|
|
335
|
+
return { success: true };
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// Logout (delete current session)
|
|
340
|
+
export const logout = mutation({
|
|
341
|
+
args: { token: v.string() },
|
|
342
|
+
handler: async (ctx, { token }) => {
|
|
343
|
+
const session = await ctx.db
|
|
344
|
+
.query("agentSessions")
|
|
345
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
|
|
346
|
+
.first();
|
|
347
|
+
|
|
348
|
+
if (session) {
|
|
349
|
+
await ctx.db.delete(session._id);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return { success: true };
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// ============================================
|
|
357
|
+
// WORKSPACE MANAGEMENT
|
|
358
|
+
// ============================================
|
|
359
|
+
|
|
360
|
+
// Update workspace tier (for Stripe webhooks)
|
|
361
|
+
export const updateTier = mutation({
|
|
362
|
+
args: {
|
|
363
|
+
workspaceId: v.id("workspaces"),
|
|
364
|
+
tier: v.string(),
|
|
365
|
+
usageLimit: v.number(),
|
|
366
|
+
stripeCustomerId: v.optional(v.string()),
|
|
367
|
+
},
|
|
368
|
+
handler: async (ctx, { workspaceId, tier, usageLimit, stripeCustomerId }) => {
|
|
369
|
+
const updates: Record<string, unknown> = {
|
|
370
|
+
tier,
|
|
371
|
+
usageLimit,
|
|
372
|
+
updatedAt: Date.now(),
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
if (stripeCustomerId) {
|
|
376
|
+
updates.stripeCustomerId = stripeCustomerId;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
await ctx.db.patch(workspaceId, updates);
|
|
380
|
+
return { success: true };
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Increment usage count
|
|
385
|
+
export const incrementUsage = mutation({
|
|
386
|
+
args: {
|
|
387
|
+
workspaceId: v.id("workspaces"),
|
|
388
|
+
amount: v.optional(v.number()),
|
|
389
|
+
},
|
|
390
|
+
handler: async (ctx, { workspaceId, amount = 1 }) => {
|
|
391
|
+
const workspace = await ctx.db.get(workspaceId);
|
|
392
|
+
if (!workspace) {
|
|
393
|
+
throw new Error("Workspace not found");
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const newCount = workspace.usageCount + amount;
|
|
397
|
+
|
|
398
|
+
// Check if over limit
|
|
399
|
+
if (newCount > workspace.usageLimit) {
|
|
400
|
+
throw new Error("Usage limit exceeded");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
await ctx.db.patch(workspaceId, {
|
|
404
|
+
usageCount: newCount,
|
|
405
|
+
updatedAt: Date.now(),
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
success: true,
|
|
410
|
+
usageCount: newCount,
|
|
411
|
+
usageRemaining: workspace.usageLimit - newCount,
|
|
412
|
+
};
|
|
413
|
+
},
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// ============================================
|
|
417
|
+
// POLLING & VERIFICATION ENDPOINTS (for HTTP API)
|
|
418
|
+
// ============================================
|
|
419
|
+
|
|
420
|
+
// Poll magic link status (for agents to check if user clicked)
|
|
421
|
+
export const pollMagicLink = query({
|
|
422
|
+
args: { token: v.string() },
|
|
423
|
+
handler: async (ctx, { token }) => {
|
|
424
|
+
const magicLink = await ctx.db
|
|
425
|
+
.query("workspaceMagicLinks")
|
|
426
|
+
.withIndex("by_token", (q) => q.eq("token", token))
|
|
427
|
+
.first();
|
|
428
|
+
|
|
429
|
+
if (!magicLink) {
|
|
430
|
+
return { status: "not_found" };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const now = Date.now();
|
|
434
|
+
|
|
435
|
+
if (magicLink.usedAt) {
|
|
436
|
+
// Get the workspace and session
|
|
437
|
+
const workspace = await ctx.db
|
|
438
|
+
.query("workspaces")
|
|
439
|
+
.withIndex("by_email", (q) => q.eq("email", magicLink.email))
|
|
440
|
+
.first();
|
|
441
|
+
|
|
442
|
+
// Get the latest session for this workspace
|
|
443
|
+
const session = workspace
|
|
444
|
+
? await ctx.db
|
|
445
|
+
.query("agentSessions")
|
|
446
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", workspace._id))
|
|
447
|
+
.order("desc")
|
|
448
|
+
.first()
|
|
449
|
+
: null;
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
status: "verified",
|
|
453
|
+
workspace: workspace
|
|
454
|
+
? {
|
|
455
|
+
id: workspace._id,
|
|
456
|
+
email: workspace.email,
|
|
457
|
+
tier: workspace.tier,
|
|
458
|
+
usageCount: workspace.usageCount,
|
|
459
|
+
usageLimit: workspace.usageLimit,
|
|
460
|
+
}
|
|
461
|
+
: null,
|
|
462
|
+
sessionToken: session?.sessionToken,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (magicLink.expiresAt < now) {
|
|
467
|
+
return { status: "expired" };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
status: "pending",
|
|
472
|
+
expiresAt: magicLink.expiresAt,
|
|
473
|
+
};
|
|
474
|
+
},
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// Verify session token (for HTTP API)
|
|
478
|
+
export const verifySession = query({
|
|
479
|
+
args: { sessionToken: v.string() },
|
|
480
|
+
handler: async (ctx, { sessionToken }) => {
|
|
481
|
+
const session = await ctx.db
|
|
482
|
+
.query("agentSessions")
|
|
483
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", sessionToken))
|
|
484
|
+
.first();
|
|
485
|
+
|
|
486
|
+
if (!session) {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const workspace = await ctx.db.get(session.workspaceId);
|
|
491
|
+
if (!workspace || workspace.status !== "active") {
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return {
|
|
496
|
+
workspaceId: workspace._id,
|
|
497
|
+
email: workspace.email,
|
|
498
|
+
tier: workspace.tier,
|
|
499
|
+
usageCount: workspace.usageCount,
|
|
500
|
+
usageLimit: workspace.usageLimit,
|
|
501
|
+
};
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// Get workspace by email (for HTTP API)
|
|
506
|
+
export const getByEmail = query({
|
|
507
|
+
args: { email: v.string() },
|
|
508
|
+
handler: async (ctx, { email }) => {
|
|
509
|
+
const workspace = await ctx.db
|
|
510
|
+
.query("workspaces")
|
|
511
|
+
.withIndex("by_email", (q) => q.eq("email", email.toLowerCase()))
|
|
512
|
+
.first();
|
|
513
|
+
|
|
514
|
+
if (!workspace) {
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
id: workspace._id,
|
|
520
|
+
email: workspace.email,
|
|
521
|
+
status: workspace.status,
|
|
522
|
+
tier: workspace.tier,
|
|
523
|
+
usageCount: workspace.usageCount,
|
|
524
|
+
usageLimit: workspace.usageLimit,
|
|
525
|
+
};
|
|
526
|
+
},
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
// Touch session (update lastUsedAt)
|
|
530
|
+
export const touchSession = mutation({
|
|
531
|
+
args: { sessionToken: v.string() },
|
|
532
|
+
handler: async (ctx, { sessionToken }) => {
|
|
533
|
+
const session = await ctx.db
|
|
534
|
+
.query("agentSessions")
|
|
535
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", sessionToken))
|
|
536
|
+
.first();
|
|
537
|
+
|
|
538
|
+
if (session) {
|
|
539
|
+
await ctx.db.patch(session._id, { lastUsedAt: Date.now() });
|
|
540
|
+
}
|
|
541
|
+
},
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// ============================================
|
|
545
|
+
// MCP WORKSPACE FUNCTIONS
|
|
546
|
+
// ============================================
|
|
547
|
+
|
|
548
|
+
// Create a new workspace (called from MCP register_owner)
|
|
549
|
+
export const createWorkspace = mutation({
|
|
550
|
+
args: { email: v.string() },
|
|
551
|
+
handler: async (ctx, { email }) => {
|
|
552
|
+
const normalizedEmail = email.toLowerCase().trim();
|
|
553
|
+
|
|
554
|
+
// Check if workspace exists
|
|
555
|
+
const existing = await ctx.db
|
|
556
|
+
.query("workspaces")
|
|
557
|
+
.withIndex("by_email", (q) => q.eq("email", normalizedEmail))
|
|
558
|
+
.first();
|
|
559
|
+
|
|
560
|
+
if (existing) {
|
|
561
|
+
return {
|
|
562
|
+
success: false,
|
|
563
|
+
error: "workspace_exists",
|
|
564
|
+
workspaceId: existing._id,
|
|
565
|
+
status: existing.status,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Create new workspace
|
|
570
|
+
const workspaceId = await ctx.db.insert("workspaces", {
|
|
571
|
+
email: normalizedEmail,
|
|
572
|
+
status: "pending",
|
|
573
|
+
tier: "free",
|
|
574
|
+
usageCount: 0,
|
|
575
|
+
usageLimit: 100, // Free tier limit
|
|
576
|
+
createdAt: Date.now(),
|
|
577
|
+
updatedAt: Date.now(),
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
return { success: true, workspaceId };
|
|
581
|
+
},
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
// Create agent session for workspace (called from MCP after verification)
|
|
585
|
+
export const createAgentSession = mutation({
|
|
586
|
+
args: {
|
|
587
|
+
workspaceId: v.id("workspaces"),
|
|
588
|
+
fingerprint: v.optional(v.string()),
|
|
589
|
+
},
|
|
590
|
+
handler: async (ctx, { workspaceId, fingerprint }) => {
|
|
591
|
+
const workspace = await ctx.db.get(workspaceId);
|
|
592
|
+
if (!workspace) {
|
|
593
|
+
return { success: false, error: "workspace_not_found" };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (workspace.status !== "active") {
|
|
597
|
+
return { success: false, error: "workspace_not_active" };
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const sessionToken = "apiclaw_" + generateToken();
|
|
601
|
+
|
|
602
|
+
await ctx.db.insert("agentSessions", {
|
|
603
|
+
workspaceId,
|
|
604
|
+
sessionToken,
|
|
605
|
+
fingerprint,
|
|
606
|
+
lastUsedAt: Date.now(),
|
|
607
|
+
createdAt: Date.now(),
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
return { success: true, sessionToken };
|
|
611
|
+
},
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// ============================================
|
|
615
|
+
// HELPER FUNCTIONS
|
|
616
|
+
// ============================================
|
|
617
|
+
|
|
618
|
+
function generateToken(): string {
|
|
619
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
620
|
+
let result = "";
|
|
621
|
+
for (let i = 0; i < 48; i++) {
|
|
622
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
623
|
+
}
|
|
624
|
+
return result;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Get workspace status (for MCP check_workspace_status tool)
|
|
628
|
+
export const getWorkspaceStatus = query({
|
|
629
|
+
args: {
|
|
630
|
+
sessionToken: v.string(),
|
|
631
|
+
},
|
|
632
|
+
handler: async (ctx, args) => {
|
|
633
|
+
const session = await ctx.db
|
|
634
|
+
.query("agentSessions")
|
|
635
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.sessionToken))
|
|
636
|
+
.first();
|
|
637
|
+
|
|
638
|
+
if (!session) {
|
|
639
|
+
return { authenticated: false };
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const workspace = await ctx.db.get(session.workspaceId);
|
|
643
|
+
if (!workspace) {
|
|
644
|
+
return { authenticated: false };
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const usageRemaining = workspace.usageLimit > 0
|
|
648
|
+
? workspace.usageLimit - workspace.usageCount
|
|
649
|
+
: -1; // -1 = unlimited
|
|
650
|
+
|
|
651
|
+
return {
|
|
652
|
+
authenticated: true,
|
|
653
|
+
email: workspace.email,
|
|
654
|
+
status: workspace.status,
|
|
655
|
+
tier: workspace.tier,
|
|
656
|
+
usageCount: workspace.usageCount,
|
|
657
|
+
usageLimit: workspace.usageLimit,
|
|
658
|
+
usageRemaining,
|
|
659
|
+
hasStripe: !!workspace.stripeCustomerId,
|
|
660
|
+
createdAt: workspace.createdAt,
|
|
661
|
+
};
|
|
662
|
+
},
|
|
663
|
+
});
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;GAGG;AA6NH,wBAAsB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CA8F9C"}
|