@nordsym/apiclaw 2.1.0 → 2.2.1

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 (185) hide show
  1. package/README.md +15 -2
  2. package/dist/bin-http.js +0 -0
  3. package/dist/bin.bundled.js +79288 -0
  4. package/dist/funnel-client.d.ts +24 -0
  5. package/dist/funnel-client.d.ts.map +1 -0
  6. package/dist/funnel-client.js +131 -0
  7. package/dist/funnel-client.js.map +1 -0
  8. package/dist/funnel.test.d.ts +2 -0
  9. package/dist/funnel.test.d.ts.map +1 -0
  10. package/dist/funnel.test.js +145 -0
  11. package/dist/funnel.test.js.map +1 -0
  12. package/dist/gateway-client.d.ts.map +1 -1
  13. package/dist/gateway-client.js +24 -2
  14. package/dist/gateway-client.js.map +1 -1
  15. package/dist/index.bundled.js +61263 -0
  16. package/dist/index.js +161 -74
  17. package/dist/index.js.map +1 -1
  18. package/dist/postinstall.d.ts +0 -5
  19. package/dist/postinstall.d.ts.map +1 -1
  20. package/dist/postinstall.js +24 -3
  21. package/dist/postinstall.js.map +1 -1
  22. package/dist/registration-guard.d.ts +29 -0
  23. package/dist/registration-guard.d.ts.map +1 -0
  24. package/dist/registration-guard.js +87 -0
  25. package/dist/registration-guard.js.map +1 -0
  26. package/package.json +7 -2
  27. package/.claude/settings.local.json +0 -9
  28. package/.env.prod +0 -1
  29. package/apiclaw-README.md +0 -494
  30. package/convex/_generated/api.d.ts +0 -137
  31. package/convex/_generated/api.js +0 -23
  32. package/convex/_generated/dataModel.d.ts +0 -60
  33. package/convex/_generated/server.d.ts +0 -143
  34. package/convex/_generated/server.js +0 -93
  35. package/convex/adminActivate.ts +0 -53
  36. package/convex/adminStats.ts +0 -306
  37. package/convex/agents.ts +0 -939
  38. package/convex/analytics.ts +0 -187
  39. package/convex/apiKeys.ts +0 -220
  40. package/convex/backfillAnalytics.ts +0 -272
  41. package/convex/backfillSearchLogs.ts +0 -35
  42. package/convex/billing.ts +0 -834
  43. package/convex/capabilities.ts +0 -157
  44. package/convex/chains.ts +0 -1318
  45. package/convex/credits.ts +0 -211
  46. package/convex/crons.ts +0 -50
  47. package/convex/debugFilestackLogs.ts +0 -16
  48. package/convex/debugGetToken.ts +0 -18
  49. package/convex/directCall.ts +0 -713
  50. package/convex/earnProgress.ts +0 -753
  51. package/convex/email.ts +0 -329
  52. package/convex/feedback.ts +0 -265
  53. package/convex/http.ts +0 -3430
  54. package/convex/inbound.ts +0 -32
  55. package/convex/logs.ts +0 -701
  56. package/convex/migrateFilestack.ts +0 -81
  57. package/convex/migratePartnersProd.ts +0 -174
  58. package/convex/migratePratham.ts +0 -126
  59. package/convex/migrateProviderWorkspaces.ts +0 -175
  60. package/convex/mou.ts +0 -91
  61. package/convex/providerKeys.ts +0 -289
  62. package/convex/providers.ts +0 -1135
  63. package/convex/purchases.ts +0 -183
  64. package/convex/ratelimit.ts +0 -104
  65. package/convex/schema.ts +0 -869
  66. package/convex/searchLogs.ts +0 -265
  67. package/convex/seedAPILayerAPIs.ts +0 -191
  68. package/convex/seedDirectCallConfigs.ts +0 -336
  69. package/convex/seedPratham.ts +0 -149
  70. package/convex/spendAlerts.ts +0 -442
  71. package/convex/stripeActions.ts +0 -607
  72. package/convex/teams.ts +0 -243
  73. package/convex/telemetry.ts +0 -81
  74. package/convex/tsconfig.json +0 -25
  75. package/convex/updateAPIStatus.ts +0 -44
  76. package/convex/usage.ts +0 -260
  77. package/convex/usageReports.ts +0 -357
  78. package/convex/waitlist.ts +0 -55
  79. package/convex/webhooks.ts +0 -494
  80. package/convex/workspaceSettings.ts +0 -143
  81. package/convex/workspaces.ts +0 -1331
  82. package/convex.json +0 -3
  83. package/direct-test.mjs +0 -51
  84. package/email-templates/filestack-provider-outreach.html +0 -162
  85. package/email-templates/partnership-template.html +0 -116
  86. package/email-templates/pratham-draft-preview.txt +0 -57
  87. package/email-templates/pratham-partnership-draft.html +0 -141
  88. package/reports/APIClaw-Session-Report-2026-04-05.pdf +0 -0
  89. package/reports/pipeline/PIPELINE-REPORT.json +0 -153
  90. package/reports/pipeline/acquire_apisguru.json +0 -17
  91. package/reports/pipeline/capabilities.json +0 -38
  92. package/reports/pipeline/discover_azure_recursive.json +0 -1551
  93. package/reports/pipeline/discover_github.json +0 -25
  94. package/reports/pipeline/discover_github_repos.json +0 -49
  95. package/reports/pipeline/discover_swaggerhub.json +0 -24
  96. package/reports/pipeline/discover_well_known.json +0 -23
  97. package/reports/pipeline/fetch_specs.json +0 -19
  98. package/reports/pipeline/generate_providers.json +0 -14
  99. package/reports/pipeline/match_registry.json +0 -11
  100. package/reports/pipeline/parse_specs.json +0 -17
  101. package/reports/pipeline/promote_candidates.json +0 -34
  102. package/reports/pipeline/validate.json +0 -30
  103. package/reports/pipeline/validate_smoke_details.json +0 -3835
  104. package/reports/session-report-2026-04-05.html +0 -433
  105. package/seed-apis-direct.mjs +0 -106
  106. package/src/access-control.ts +0 -174
  107. package/src/adapters/base.ts +0 -364
  108. package/src/adapters/claude-desktop.ts +0 -41
  109. package/src/adapters/cline.ts +0 -88
  110. package/src/adapters/continue.ts +0 -91
  111. package/src/adapters/cursor.ts +0 -43
  112. package/src/adapters/custom.ts +0 -188
  113. package/src/adapters/detect.ts +0 -202
  114. package/src/adapters/index.ts +0 -47
  115. package/src/adapters/windsurf.ts +0 -44
  116. package/src/bin-http.ts +0 -45
  117. package/src/bin.ts +0 -34
  118. package/src/capability-router.ts +0 -331
  119. package/src/chainExecutor.ts +0 -730
  120. package/src/chainResolver.test.ts +0 -246
  121. package/src/chainResolver.ts +0 -658
  122. package/src/cli/commands/demo.ts +0 -109
  123. package/src/cli/commands/doctor.ts +0 -435
  124. package/src/cli/commands/index.ts +0 -9
  125. package/src/cli/commands/login.ts +0 -203
  126. package/src/cli/commands/mcp-install.ts +0 -373
  127. package/src/cli/commands/restore.ts +0 -333
  128. package/src/cli/commands/setup.ts +0 -297
  129. package/src/cli/commands/uninstall.ts +0 -240
  130. package/src/cli/index.ts +0 -148
  131. package/src/cli.ts +0 -370
  132. package/src/confirmation.ts +0 -296
  133. package/src/credentials.ts +0 -455
  134. package/src/credits.ts +0 -329
  135. package/src/crypto.ts +0 -75
  136. package/src/discovery.ts +0 -568
  137. package/src/enterprise/env.ts +0 -156
  138. package/src/enterprise/index.ts +0 -7
  139. package/src/enterprise/script-generator.ts +0 -481
  140. package/src/execute-dynamic.ts +0 -617
  141. package/src/execute.ts +0 -2386
  142. package/src/gateway-client.ts +0 -192
  143. package/src/hivr-whitelist.ts +0 -110
  144. package/src/http-api.ts +0 -286
  145. package/src/http-server-minimal.ts +0 -154
  146. package/src/index.ts +0 -2611
  147. package/src/intelligent-gateway.ts +0 -339
  148. package/src/mcp-analytics.ts +0 -156
  149. package/src/metered.ts +0 -149
  150. package/src/open-apis-generated.ts +0 -157
  151. package/src/open-apis.ts +0 -558
  152. package/src/postinstall.ts +0 -18
  153. package/src/product-whitelist.ts +0 -246
  154. package/src/proxy.ts +0 -36
  155. package/src/session.ts +0 -129
  156. package/src/stripe.ts +0 -497
  157. package/src/telemetry.ts +0 -71
  158. package/src/test.ts +0 -135
  159. package/src/types/convex-api.d.ts +0 -20
  160. package/src/types/convex-api.ts +0 -21
  161. package/src/types.ts +0 -109
  162. package/src/ui/colors.ts +0 -219
  163. package/src/ui/errors.ts +0 -394
  164. package/src/ui/index.ts +0 -17
  165. package/src/ui/prompts.ts +0 -390
  166. package/src/ui/spinner.ts +0 -325
  167. package/src/utils/backup.ts +0 -224
  168. package/src/utils/config.ts +0 -318
  169. package/src/utils/os.ts +0 -124
  170. package/src/utils/paths.ts +0 -203
  171. package/src/webhook.ts +0 -107
  172. package/test-10-working.cjs +0 -97
  173. package/test-14-final.cjs +0 -96
  174. package/test-actual-handlers.ts +0 -92
  175. package/test-apilayer-all-14.ts +0 -249
  176. package/test-apilayer-fixed.ts +0 -248
  177. package/test-direct-endpoints.ts +0 -174
  178. package/test-exact-endpoints.ts +0 -144
  179. package/test-final.ts +0 -83
  180. package/test-full-routing.ts +0 -100
  181. package/test-handlers-correct.ts +0 -217
  182. package/test-numverify-key.ts +0 -41
  183. package/test-via-handlers.ts +0 -92
  184. package/test-worldnews.mjs +0 -26
  185. package/tsconfig.json +0 -20
@@ -1,1331 +0,0 @@
1
- import { mutation, query } from "./_generated/server";
2
- import { internal } from "./_generated/api";
3
- import { v } from "convex/values";
4
-
5
- // ============================================
6
- // OTP AUTH FOR WORKSPACES (terminal-native)
7
- // ============================================
8
-
9
- function generateOTP(): string {
10
- const digits = "0123456789";
11
- let code = "";
12
- for (let i = 0; i < 6; i++) {
13
- code += digits.charAt(Math.floor(Math.random() * digits.length));
14
- }
15
- return code;
16
- }
17
-
18
- // Create OTP code and return it (MCP server sends the email)
19
- export const createOTP = mutation({
20
- args: {
21
- email: v.string(),
22
- fingerprint: v.optional(v.string()),
23
- },
24
- handler: async (ctx, { email, fingerprint }) => {
25
- const normalizedEmail = email.toLowerCase().trim();
26
-
27
- // Invalidate any existing unused OTPs for this email
28
- const existing = await ctx.db
29
- .query("otpCodes")
30
- .withIndex("by_email", (q) => q.eq("email", normalizedEmail))
31
- .collect();
32
- for (const otp of existing) {
33
- if (!otp.usedAt && otp.expiresAt > Date.now()) {
34
- await ctx.db.patch(otp._id, { expiresAt: 0 }); // expire it
35
- }
36
- }
37
-
38
- const code = generateOTP();
39
- const expiresAt = Date.now() + 10 * 60 * 1000; // 10 minutes
40
-
41
- await ctx.db.insert("otpCodes", {
42
- email: normalizedEmail,
43
- code,
44
- fingerprint,
45
- expiresAt,
46
- usedAt: undefined,
47
- attempts: 0,
48
- createdAt: Date.now(),
49
- });
50
-
51
- return { code, expiresAt };
52
- },
53
- });
54
-
55
- // Verify OTP code, create/activate workspace, return session
56
- export const verifyOTP = mutation({
57
- args: {
58
- email: v.string(),
59
- code: v.string(),
60
- fingerprint: v.optional(v.string()),
61
- },
62
- handler: async (ctx, { email, code, fingerprint }) => {
63
- const normalizedEmail = email.toLowerCase().trim();
64
-
65
- const otpRecord = await ctx.db
66
- .query("otpCodes")
67
- .withIndex("by_email_code", (q) =>
68
- q.eq("email", normalizedEmail).eq("code", code)
69
- )
70
- .first();
71
-
72
- if (!otpRecord) {
73
- return { success: false, error: "invalid_code", message: "Invalid verification code." };
74
- }
75
-
76
- if (otpRecord.usedAt) {
77
- return { success: false, error: "code_used", message: "Code already used." };
78
- }
79
-
80
- if (otpRecord.expiresAt < Date.now()) {
81
- return { success: false, error: "code_expired", message: "Code expired. Run register_owner again to get a new code." };
82
- }
83
-
84
- if (otpRecord.attempts >= 5) {
85
- return { success: false, error: "too_many_attempts", message: "Too many failed attempts. Run register_owner again." };
86
- }
87
-
88
- // Mark OTP as used
89
- await ctx.db.patch(otpRecord._id, { usedAt: Date.now() });
90
-
91
- // Find or create workspace
92
- let workspace = await ctx.db
93
- .query("workspaces")
94
- .withIndex("by_email", (q) => q.eq("email", normalizedEmail))
95
- .first();
96
-
97
- let isNewUser = false;
98
- if (!workspace) {
99
- isNewUser = true;
100
- // Generate referral code
101
- let referralCode: string;
102
- let attempts = 0;
103
- do {
104
- const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
105
- let rc = "";
106
- for (let i = 0; i < 6; i++) {
107
- rc += chars.charAt(Math.floor(Math.random() * chars.length));
108
- }
109
- referralCode = `CLAW-${rc}`;
110
- const existingRef = await ctx.db
111
- .query("workspaces")
112
- .withIndex("by_referralCode", (q) => q.eq("referralCode", referralCode))
113
- .first();
114
- if (!existingRef) break;
115
- attempts++;
116
- } while (attempts < 10);
117
-
118
- const workspaceId = await ctx.db.insert("workspaces", {
119
- email: normalizedEmail,
120
- status: "active",
121
- tier: "free",
122
- usageCount: 0,
123
- usageLimit: 50,
124
- weeklyUsageCount: 0,
125
- weeklyUsageLimit: 50,
126
- lastWeeklyResetAt: Date.now(),
127
- hourlyUsageCount: 0,
128
- lastHourlyResetAt: Date.now(),
129
- referralCode: referralCode!,
130
- createdAt: Date.now(),
131
- updatedAt: Date.now(),
132
- });
133
- workspace = await ctx.db.get(workspaceId);
134
- } else if (workspace.status === "pending") {
135
- // Activate pending workspace
136
- await ctx.db.patch(workspace._id, { status: "active" });
137
- workspace = await ctx.db.get(workspace._id);
138
- }
139
-
140
- if (!workspace) {
141
- return { success: false, error: "workspace_error", message: "Failed to create workspace." };
142
- }
143
-
144
- // Create agent session
145
- const sessionToken = generateToken();
146
- await ctx.db.insert("agentSessions", {
147
- workspaceId: workspace._id,
148
- sessionToken,
149
- fingerprint: fingerprint || "unknown",
150
- lastUsedAt: Date.now(),
151
- createdAt: Date.now(),
152
- });
153
-
154
- return {
155
- success: true,
156
- isNewUser,
157
- sessionToken,
158
- workspace: {
159
- id: workspace._id,
160
- email: workspace.email,
161
- tier: workspace.tier,
162
- status: "active",
163
- usageCount: workspace.usageCount,
164
- usageLimit: workspace.usageLimit,
165
- },
166
- };
167
- },
168
- });
169
-
170
- // Increment failed OTP attempt counter
171
- export const incrementOTPAttempt = mutation({
172
- args: {
173
- email: v.string(),
174
- code: v.string(),
175
- },
176
- handler: async (ctx, { email, code }) => {
177
- const normalizedEmail = email.toLowerCase().trim();
178
- const otpRecord = await ctx.db
179
- .query("otpCodes")
180
- .withIndex("by_email_code", (q) =>
181
- q.eq("email", normalizedEmail).eq("code", code)
182
- )
183
- .first();
184
- if (otpRecord && !otpRecord.usedAt) {
185
- await ctx.db.patch(otpRecord._id, { attempts: otpRecord.attempts + 1 });
186
- }
187
- },
188
- });
189
-
190
- // ============================================
191
- // MAGIC LINK AUTH FOR WORKSPACES
192
- // ============================================
193
-
194
- // Create magic link for workspace email auth
195
- export const createMagicLink = mutation({
196
- args: {
197
- email: v.string(),
198
- fingerprint: v.optional(v.string()),
199
- },
200
- handler: async (ctx, { email, fingerprint }) => {
201
- const token = generateToken();
202
- const expiresAt = Date.now() + 15 * 60 * 1000; // 15 minutes
203
-
204
- await ctx.db.insert("workspaceMagicLinks", {
205
- email: email.toLowerCase(),
206
- token,
207
- sessionFingerprint: fingerprint,
208
- expiresAt,
209
- createdAt: Date.now(),
210
- });
211
-
212
- return { token, expiresAt };
213
- },
214
- });
215
-
216
- // Generate a unique referral code (CLAW-XXXXXX format)
217
- function generateReferralCode(): string {
218
- const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
219
- let code = "";
220
- for (let i = 0; i < 6; i++) {
221
- code += chars.charAt(Math.floor(Math.random() * chars.length));
222
- }
223
- return `CLAW-${code}`;
224
- }
225
-
226
- // Verify magic link and create workspace + session
227
- export const verifyMagicLink = mutation({
228
- args: {
229
- token: v.string(),
230
- fingerprint: v.optional(v.string()),
231
- referralCode: v.optional(v.string()), // Referral code from signup URL
232
- },
233
- handler: async (ctx, { token, fingerprint, referralCode }) => {
234
- const magicLink = await ctx.db
235
- .query("workspaceMagicLinks")
236
- .withIndex("by_token", (q) => q.eq("token", token))
237
- .first();
238
-
239
- if (!magicLink) {
240
- return { success: false, error: "Invalid token" };
241
- }
242
-
243
- if (magicLink.expiresAt < Date.now()) {
244
- return { success: false, error: "Token expired" };
245
- }
246
-
247
- if (magicLink.usedAt) {
248
- return { success: false, error: "Token already used" };
249
- }
250
-
251
- // Mark as used
252
- await ctx.db.patch(magicLink._id, { usedAt: Date.now() });
253
-
254
- // Find or create workspace
255
- let workspace = await ctx.db
256
- .query("workspaces")
257
- .withIndex("by_email", (q) => q.eq("email", magicLink.email))
258
- .first();
259
-
260
- let isNewUser = false;
261
- if (!workspace) {
262
- isNewUser = true;
263
-
264
- // Generate unique referral code for new user
265
- let newReferralCode: string;
266
- let attempts = 0;
267
- do {
268
- newReferralCode = generateReferralCode();
269
- const existing = await ctx.db
270
- .query("workspaces")
271
- .withIndex("by_referralCode", (q) => q.eq("referralCode", newReferralCode))
272
- .first();
273
- if (!existing) break;
274
- attempts++;
275
- } while (attempts < 10);
276
-
277
- // Create new workspace with free tier + referral code
278
- const workspaceId = await ctx.db.insert("workspaces", {
279
- email: magicLink.email,
280
- status: "active",
281
- tier: "free",
282
- usageCount: 0,
283
- usageLimit: 50, // 50 calls/month for free tier
284
- weeklyUsageCount: 0,
285
- weeklyUsageLimit: 50, // Monthly limit (field name is legacy)
286
- hourlyUsageCount: 0,
287
- referralCode: newReferralCode!,
288
- createdAt: Date.now(),
289
- updatedAt: Date.now(),
290
- });
291
- workspace = await ctx.db.get(workspaceId);
292
- }
293
-
294
- // REFERRAL DISABLED (2026-03-01): Risk of abuse with awesome-list exposure
295
- // Tracking referredBy for analytics only, no credit bonus
296
- if (isNewUser && referralCode) {
297
- const referrer = await ctx.db
298
- .query("workspaces")
299
- .withIndex("by_referralCode", (q) => q.eq("referralCode", referralCode))
300
- .first();
301
-
302
- if (referrer && referrer._id !== workspace!._id) {
303
- // Track referral for analytics only
304
- await ctx.db.patch(workspace!._id, {
305
- referredBy: referrer._id,
306
- updatedAt: Date.now(),
307
- });
308
- // No credit bonus - referral rewards disabled
309
- }
310
- }
311
-
312
- // Reuse existing session for same machine (fix: no more duplicate sessions per login)
313
- const sessionToken = generateToken();
314
- const userFingerprint2 = fingerprint || magicLink.sessionFingerprint;
315
-
316
- const existingSession = userFingerprint2
317
- ? await ctx.db
318
- .query("agentSessions")
319
- .withIndex("by_workspaceId", (q) => q.eq("workspaceId", workspace!._id))
320
- .filter((q) => q.eq(q.field("fingerprint"), userFingerprint2))
321
- .first()
322
- : null;
323
-
324
- if (existingSession) {
325
- // Refresh existing session instead of creating duplicate
326
- await ctx.db.patch(existingSession._id, {
327
- sessionToken,
328
- lastUsedAt: Date.now(),
329
- });
330
- } else {
331
- await ctx.db.insert("agentSessions", {
332
- workspaceId: workspace!._id,
333
- sessionToken,
334
- fingerprint: userFingerprint2 || undefined,
335
- lastUsedAt: Date.now(),
336
- createdAt: Date.now(),
337
- });
338
- }
339
-
340
- // Link agent record to workspace (if agent exists for this fingerprint)
341
- if (userFingerprint2) {
342
- const agentForFingerprint = await ctx.db
343
- .query("agents")
344
- .filter((q) => q.eq(q.field("fingerprint"), userFingerprint2))
345
- .first();
346
-
347
- if (agentForFingerprint && !agentForFingerprint.workspaceId) {
348
- await ctx.db.patch(agentForFingerprint._id, {
349
- workspaceId: workspace!._id,
350
- });
351
- }
352
- }
353
-
354
- // Claim anonymous usage history
355
- const userFingerprint = fingerprint || magicLink.sessionFingerprint;
356
- if (userFingerprint) {
357
- try {
358
- // Find all analytics records with matching fingerprint and no workspaceId
359
- const analyticsRecords = await ctx.db
360
- .query("analytics")
361
- .withIndex("by_identifier", (q) => q.eq("identifier", userFingerprint))
362
- .collect();
363
-
364
- // Filter to only unclaimed records
365
- const unclaimedRecords = analyticsRecords.filter((r) => !r.workspaceId);
366
-
367
- // Update each record to link it to the workspace
368
- for (const record of unclaimedRecords) {
369
- await ctx.db.patch(record._id, { workspaceId: workspace!._id });
370
- }
371
- } catch (err) {
372
- // Non-critical error, just log it
373
- console.error('Failed to claim anonymous usage:', err);
374
- }
375
- }
376
-
377
- // Notify Inbound Net (ALERTS) — async, non-blocking
378
- await ctx.scheduler.runAfter(0, internal.inbound.notifySignup, {
379
- email: workspace!.email,
380
- workspaceId: workspace!._id,
381
- tier: workspace!.tier,
382
- isNewUser,
383
- timestamp: Date.now(),
384
- });
385
-
386
- return {
387
- success: true,
388
- sessionToken,
389
- workspace: {
390
- id: workspace!._id,
391
- email: workspace!.email,
392
- tier: workspace!.tier,
393
- referralCode: workspace!.referralCode,
394
- },
395
- };
396
- },
397
- });
398
-
399
- // Get session from token
400
- export const getSession = query({
401
- args: { token: v.string() },
402
- handler: async (ctx, { token }) => {
403
- const session = await ctx.db
404
- .query("agentSessions")
405
- .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
406
- .first();
407
-
408
- if (!session) {
409
- return null;
410
- }
411
-
412
- const workspace = await ctx.db.get(session.workspaceId);
413
- if (!workspace) return null;
414
-
415
- return {
416
- workspaceId: workspace._id,
417
- email: workspace.email,
418
- tier: workspace.tier,
419
- status: workspace.status,
420
- usageCount: workspace.usageCount,
421
- usageLimit: workspace.usageLimit,
422
- };
423
- },
424
- });
425
-
426
- // ============================================
427
- // DASHBOARD QUERIES
428
- // ============================================
429
-
430
- // Get full workspace dashboard data
431
- export const getWorkspaceDashboard = query({
432
- args: { token: v.string() },
433
- handler: async (ctx, { token }) => {
434
- // Verify session
435
- const session = await ctx.db
436
- .query("agentSessions")
437
- .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
438
- .first();
439
-
440
- if (!session) {
441
- return null;
442
- }
443
-
444
- // Note: lastUsedAt is updated via touchSession mutation separately
445
-
446
- const workspace = await ctx.db.get(session.workspaceId);
447
- if (!workspace) return null;
448
-
449
- // Get all agent sessions for this workspace
450
- const agentSessions = await ctx.db
451
- .query("agentSessions")
452
- .withIndex("by_workspaceId", (q) => q.eq("workspaceId", session.workspaceId))
453
- .collect();
454
-
455
- // Count agents: 1 main agent (if exists) + subagents
456
- const hasMainAgent = workspace.mainAgentId ? 1 : 0;
457
- const subagents = await ctx.db
458
- .query("subagents")
459
- .withIndex("by_workspaceId", (q) => q.eq("workspaceId", session.workspaceId))
460
- .collect();
461
-
462
- const totalAgentCount = hasMainAgent + subagents.length;
463
-
464
- // Get usage logs for this workspace (via agent credits or purchases)
465
- const credits = await ctx.db
466
- .query("agentCredits")
467
- .collect();
468
-
469
- // Filter credits that belong to this workspace's agents
470
- const workspaceCredits = credits.filter(c =>
471
- agentSessions.some(s => c.agentId === s.sessionToken)
472
- );
473
-
474
- // Get purchases for workspace agents
475
- const purchases = await ctx.db
476
- .query("purchases")
477
- .collect();
478
-
479
- const workspacePurchases = purchases.filter(p =>
480
- agentSessions.some(s => p.agentId === s.sessionToken)
481
- );
482
-
483
- // Calculate usage remaining -- paid tiers have high limits
484
- const now = Date.now();
485
- const isPaidTier = ["pro", "scale", "usage_based", "partner", "founder"].includes(workspace.tier);
486
- const effectiveLimit = isPaidTier ? -1 : workspace.usageLimit; // -1 = unlimited
487
- const usageRemaining = isPaidTier ? -1 : Math.max(0, workspace.usageLimit - workspace.usageCount);
488
- const usagePercentage = isPaidTier ? 0 : (workspace.usageCount / workspace.usageLimit) * 100;
489
-
490
- // Budget status (PRD 2.6)
491
- const monthStart = getMonthStartForBudget();
492
- let currentSpend = workspace.monthlySpendCents || 0;
493
- if (!workspace.lastSpendResetAt || workspace.lastSpendResetAt < monthStart) {
494
- currentSpend = 0;
495
- }
496
- const budgetCap = workspace.budgetCap || null;
497
-
498
- return {
499
- workspace: {
500
- id: workspace._id,
501
- email: workspace.email,
502
- workspaceName: workspace.workspaceName,
503
- tier: workspace.tier,
504
- status: workspace.status,
505
- usageCount: workspace.usageCount,
506
- usageLimit: effectiveLimit,
507
- usageRemaining,
508
- usagePercentage,
509
- stripeCustomerId: workspace.stripeCustomerId,
510
- createdAt: workspace.createdAt,
511
- mainAgentName: workspace.mainAgentName,
512
- mainAgentId: workspace.mainAgentId,
513
- },
514
- stats: {
515
- totalAgents: totalAgentCount,
516
- totalCredits: workspaceCredits.reduce((sum, c) => sum + c.balanceUsd, 0),
517
- totalPurchases: workspacePurchases.length,
518
- },
519
- budget: {
520
- budgetCapCents: budgetCap,
521
- budgetCapUsd: budgetCap ? budgetCap / 100 : null,
522
- currentSpendCents: currentSpend,
523
- currentSpendUsd: currentSpend / 100,
524
- remainingCents: budgetCap ? Math.max(0, budgetCap - currentSpend) : null,
525
- remainingUsd: budgetCap ? Math.max(0, (budgetCap - currentSpend) / 100) : null,
526
- budgetPercentage: budgetCap ? Math.min(100, (currentSpend / budgetCap) * 100) : null,
527
- pauseOnBudgetExceeded: workspace.pauseOnBudgetExceeded || false,
528
- isOverBudget: budgetCap ? currentSpend >= budgetCap : false,
529
- isNearBudget: budgetCap ? currentSpend >= budgetCap * 0.8 : false,
530
- },
531
- };
532
- },
533
- });
534
-
535
- // Helper for budget month start
536
- function getMonthStartForBudget(): number {
537
- const now = new Date();
538
- return new Date(now.getUTCFullYear(), now.getUTCMonth(), 1, 0, 0, 0, 0).getTime();
539
- }
540
-
541
- // Get connected agents for workspace
542
- export const getConnectedAgents = query({
543
- args: { token: v.string() },
544
- handler: async (ctx, { token }) => {
545
- const session = await ctx.db
546
- .query("agentSessions")
547
- .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
548
- .first();
549
-
550
- if (!session) {
551
- return [];
552
- }
553
-
554
- const agentSessions = await ctx.db
555
- .query("agentSessions")
556
- .withIndex("by_workspaceId", (q) => q.eq("workspaceId", session.workspaceId))
557
- .collect();
558
-
559
- return agentSessions.map((s) => ({
560
- id: s._id,
561
- fingerprint: s.fingerprint || "Unknown",
562
- customName: s.customName || null,
563
- name: s.customName || s.fingerprint || "Unknown",
564
- lastUsedAt: s.lastUsedAt,
565
- createdAt: s.createdAt,
566
- isCurrent: s.sessionToken === token,
567
- }));
568
- },
569
- });
570
-
571
- // Admin: Delete session by ID (for cleanup)
572
- export const adminDeleteSession = mutation({
573
- args: { sessionId: v.id("agentSessions") },
574
- handler: async (ctx, { sessionId }) => {
575
- await ctx.db.delete(sessionId);
576
- return { success: true };
577
- },
578
- });
579
-
580
- // Debug: Get sessions by workspace email
581
- export const getSessionsByEmail = query({
582
- args: { email: v.string() },
583
- handler: async (ctx, { email }) => {
584
- const workspace = await ctx.db
585
- .query("workspaces")
586
- .withIndex("by_email", (q) => q.eq("email", email.toLowerCase()))
587
- .first();
588
-
589
- if (!workspace) {
590
- return { error: "Workspace not found", sessions: [] };
591
- }
592
-
593
- const sessions = await ctx.db
594
- .query("agentSessions")
595
- .withIndex("by_workspaceId", (q) => q.eq("workspaceId", workspace._id))
596
- .collect();
597
-
598
- return {
599
- workspaceId: workspace._id,
600
- email: workspace.email,
601
- sessions: sessions.map(s => ({
602
- id: s._id,
603
- fingerprint: s.fingerprint,
604
- createdAt: s.createdAt,
605
- lastUsedAt: s.lastUsedAt,
606
- })),
607
- };
608
- },
609
- });
610
-
611
- // Rename an agent session
612
- export const renameAgent = mutation({
613
- args: {
614
- token: v.string(),
615
- sessionId: v.id("agentSessions"),
616
- name: v.string(),
617
- },
618
- handler: async (ctx, { token, sessionId, name }) => {
619
- // Verify the requesting session
620
- const session = await ctx.db
621
- .query("agentSessions")
622
- .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
623
- .first();
624
-
625
- if (!session) {
626
- throw new Error("Invalid session");
627
- }
628
-
629
- // Get the session to rename
630
- const targetSession = await ctx.db.get(sessionId);
631
- if (!targetSession || targetSession.workspaceId !== session.workspaceId) {
632
- throw new Error("Session not found or access denied");
633
- }
634
-
635
- // Update the name (stored as customName field)
636
- await ctx.db.patch(sessionId, { customName: name });
637
-
638
- return { success: true };
639
- },
640
- });
641
-
642
- // Get usage breakdown by provider
643
- export const getUsageBreakdown = query({
644
- args: { token: v.string() },
645
- handler: async (ctx, { token }) => {
646
- const session = await ctx.db
647
- .query("agentSessions")
648
- .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
649
- .first();
650
-
651
- if (!session) {
652
- return { byProvider: [], byDay: [], total: 0 };
653
- }
654
-
655
- // Get all sessions for this workspace
656
- const agentSessions = await ctx.db
657
- .query("agentSessions")
658
- .withIndex("by_workspaceId", (q) => q.eq("workspaceId", session.workspaceId))
659
- .collect();
660
-
661
- const sessionTokens = agentSessions.map(s => s.sessionToken);
662
-
663
- // Get purchases for these agents
664
- const allPurchases = await ctx.db.query("purchases").collect();
665
- const workspacePurchases = allPurchases.filter(p => sessionTokens.includes(p.agentId));
666
-
667
- // Get usage for purchases
668
- const allUsage = await ctx.db.query("usage").collect();
669
- const purchaseIds = workspacePurchases.map(p => p._id);
670
- const workspaceUsage = allUsage.filter(u => purchaseIds.includes(u.purchaseId));
671
-
672
- // Aggregate by provider
673
- const byProvider: Record<string, { calls: number; cost: number }> = {};
674
- for (const usage of workspaceUsage) {
675
- if (!byProvider[usage.providerId]) {
676
- byProvider[usage.providerId] = { calls: 0, cost: 0 };
677
- }
678
- byProvider[usage.providerId].calls += usage.unitsUsed;
679
- byProvider[usage.providerId].cost += usage.costIncurredUsd;
680
- }
681
-
682
- // Aggregate by day (last 14 days)
683
- const now = Date.now();
684
- const fourteenDaysAgo = now - 14 * 24 * 60 * 60 * 1000;
685
- const byDay: Record<string, number> = {};
686
-
687
- for (const usage of workspaceUsage) {
688
- if (usage.lastUsedAt >= fourteenDaysAgo) {
689
- const day = new Date(usage.lastUsedAt).toISOString().split("T")[0];
690
- byDay[day] = (byDay[day] || 0) + usage.unitsUsed;
691
- }
692
- }
693
-
694
- return {
695
- byProvider: Object.entries(byProvider).map(([provider, data]) => ({
696
- provider,
697
- calls: data.calls,
698
- cost: data.cost,
699
- })),
700
- byDay: Object.entries(byDay)
701
- .map(([date, calls]) => ({ date, calls }))
702
- .sort((a, b) => a.date.localeCompare(b.date)),
703
- total: workspaceUsage.reduce((sum, u) => sum + u.unitsUsed, 0),
704
- };
705
- },
706
- });
707
-
708
- // ============================================
709
- // AGENT MANAGEMENT
710
- // ============================================
711
-
712
- // Revoke an agent session
713
- export const revokeAgentSession = mutation({
714
- args: {
715
- token: v.string(),
716
- sessionId: v.id("agentSessions"),
717
- },
718
- handler: async (ctx, { token, sessionId }) => {
719
- // Verify the requesting session
720
- const session = await ctx.db
721
- .query("agentSessions")
722
- .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
723
- .first();
724
-
725
- if (!session) {
726
- throw new Error("Unauthorized");
727
- }
728
-
729
- // Get the session to revoke
730
- const targetSession = await ctx.db.get(sessionId);
731
- if (!targetSession) {
732
- throw new Error("Session not found");
733
- }
734
-
735
- // Verify same workspace
736
- if (targetSession.workspaceId !== session.workspaceId) {
737
- throw new Error("Unauthorized");
738
- }
739
-
740
- // Prevent revoking current session
741
- if (targetSession.sessionToken === token) {
742
- throw new Error("Cannot revoke current session");
743
- }
744
-
745
- // Delete the session
746
- await ctx.db.delete(sessionId);
747
-
748
- return { success: true };
749
- },
750
- });
751
-
752
- // Logout (delete current session)
753
- export const logout = mutation({
754
- args: { token: v.string() },
755
- handler: async (ctx, { token }) => {
756
- const session = await ctx.db
757
- .query("agentSessions")
758
- .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
759
- .first();
760
-
761
- if (session) {
762
- await ctx.db.delete(session._id);
763
- }
764
-
765
- return { success: true };
766
- },
767
- });
768
-
769
- // ============================================
770
- // WORKSPACE MANAGEMENT
771
- // ============================================
772
-
773
- // Update workspace tier (for Stripe webhooks)
774
- export const updateTier = mutation({
775
- args: {
776
- workspaceId: v.id("workspaces"),
777
- tier: v.string(),
778
- usageLimit: v.number(),
779
- stripeCustomerId: v.optional(v.string()),
780
- },
781
- handler: async (ctx, { workspaceId, tier, usageLimit, stripeCustomerId }) => {
782
- const updates: Record<string, unknown> = {
783
- tier,
784
- usageLimit,
785
- updatedAt: Date.now(),
786
- };
787
-
788
- if (stripeCustomerId) {
789
- updates.stripeCustomerId = stripeCustomerId;
790
- }
791
-
792
- await ctx.db.patch(workspaceId, updates);
793
- return { success: true };
794
- },
795
- });
796
-
797
- // Increment usage count
798
- // Constants for rate limiting
799
- const FREE_WEEKLY_LIMIT = 50;
800
- const FREE_HOURLY_LIMIT = 10;
801
- // Rate limiting constants
802
-
803
- // Helper: Get start of current week (Monday 00:00 UTC)
804
- function getWeekStart(): number {
805
- const now = new Date();
806
- const dayOfWeek = now.getUTCDay();
807
- const diff = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // Monday = 0
808
- const monday = new Date(now);
809
- monday.setUTCDate(now.getUTCDate() - diff);
810
- monday.setUTCHours(0, 0, 0, 0);
811
- return monday.getTime();
812
- }
813
-
814
- // Helper: Get start of current hour
815
- function getHourStart(): number {
816
- const now = new Date();
817
- now.setUTCMinutes(0, 0, 0);
818
- return now.getTime();
819
- }
820
-
821
- export const incrementUsage = mutation({
822
- args: {
823
- workspaceId: v.id("workspaces"),
824
- amount: v.optional(v.number()),
825
- },
826
- handler: async (ctx, { workspaceId, amount = 1 }) => {
827
- const workspace = await ctx.db.get(workspaceId);
828
- if (!workspace) {
829
- throw new Error("Workspace not found");
830
- }
831
-
832
- const now = Date.now();
833
- const weekStart = getWeekStart();
834
- const hourStart = getHourStart();
835
-
836
- // Check if paid tier (unlimited usage)
837
- const isPaid = ["pro", "scale", "usage_based", "partner", "founder"].includes(workspace.tier);
838
-
839
- // Initialize weekly/hourly counters if needed
840
- let weeklyCount = workspace.weeklyUsageCount || 0;
841
- let hourlyCount = workspace.hourlyUsageCount || 0;
842
-
843
- // Reset weekly counter if new week
844
- if (!workspace.lastWeeklyResetAt || workspace.lastWeeklyResetAt < weekStart) {
845
- weeklyCount = 0;
846
- }
847
-
848
- // Reset hourly counter if new hour
849
- if (!workspace.lastHourlyResetAt || workspace.lastHourlyResetAt < hourStart) {
850
- hourlyCount = 0;
851
- }
852
-
853
- // Check rate limits for free tier
854
- if (!isPaid && workspace.tier !== "enterprise") {
855
- // Check hourly limit (10/hour for free)
856
- if (hourlyCount + amount > FREE_HOURLY_LIMIT) {
857
- throw new Error(`Hourly rate limit exceeded (${FREE_HOURLY_LIMIT}/hour). Upgrade to Pro for unlimited.`);
858
- }
859
-
860
- // Check weekly limit (50/week for free)
861
- if (weeklyCount + amount > FREE_WEEKLY_LIMIT) {
862
- throw new Error(`Weekly limit exceeded (${FREE_WEEKLY_LIMIT}/week). Upgrade to Pro for unlimited.`);
863
- }
864
- }
865
-
866
- const newTotalCount = workspace.usageCount + amount;
867
- const newWeeklyCount = weeklyCount + amount;
868
- const newHourlyCount = hourlyCount + amount;
869
-
870
- await ctx.db.patch(workspaceId, {
871
- usageCount: newTotalCount,
872
- weeklyUsageCount: newWeeklyCount,
873
- hourlyUsageCount: newHourlyCount,
874
- lastWeeklyResetAt: weekStart,
875
- lastHourlyResetAt: hourStart,
876
- updatedAt: now,
877
- });
878
-
879
- // Calculate remaining for free tier
880
- const weeklyRemaining = isPaid ? Infinity : Math.max(0, FREE_WEEKLY_LIMIT - newWeeklyCount);
881
- const hourlyRemaining = isPaid ? Infinity : Math.max(0, FREE_HOURLY_LIMIT - newHourlyCount);
882
-
883
- return {
884
- success: true,
885
- usageCount: newTotalCount,
886
- weeklyUsageCount: newWeeklyCount,
887
- weeklyRemaining,
888
- hourlyRemaining,
889
- isPaid,
890
- };
891
- },
892
- });
893
-
894
- // ============================================
895
- // POLLING & VERIFICATION ENDPOINTS (for HTTP API)
896
- // ============================================
897
-
898
- // Poll magic link status (for agents to check if user clicked)
899
- export const pollMagicLink = query({
900
- args: { token: v.string() },
901
- handler: async (ctx, { token }) => {
902
- const magicLink = await ctx.db
903
- .query("workspaceMagicLinks")
904
- .withIndex("by_token", (q) => q.eq("token", token))
905
- .first();
906
-
907
- if (!magicLink) {
908
- return { status: "not_found" };
909
- }
910
-
911
- const now = Date.now();
912
-
913
- if (magicLink.usedAt) {
914
- // Get the workspace and session
915
- const workspace = await ctx.db
916
- .query("workspaces")
917
- .withIndex("by_email", (q) => q.eq("email", magicLink.email))
918
- .first();
919
-
920
- // Get the latest session for this workspace
921
- const session = workspace
922
- ? await ctx.db
923
- .query("agentSessions")
924
- .withIndex("by_workspaceId", (q) => q.eq("workspaceId", workspace._id))
925
- .order("desc")
926
- .first()
927
- : null;
928
-
929
- return {
930
- status: "verified",
931
- workspace: workspace
932
- ? {
933
- id: workspace._id,
934
- email: workspace.email,
935
- tier: workspace.tier,
936
- usageCount: workspace.usageCount,
937
- usageLimit: workspace.usageLimit,
938
- }
939
- : null,
940
- sessionToken: session?.sessionToken,
941
- };
942
- }
943
-
944
- if (magicLink.expiresAt < now) {
945
- return { status: "expired" };
946
- }
947
-
948
- return {
949
- status: "pending",
950
- expiresAt: magicLink.expiresAt,
951
- };
952
- },
953
- });
954
-
955
- // Verify session token (for HTTP API)
956
- export const verifySession = query({
957
- args: { sessionToken: v.string() },
958
- handler: async (ctx, { sessionToken }) => {
959
- const session = await ctx.db
960
- .query("agentSessions")
961
- .withIndex("by_sessionToken", (q) => q.eq("sessionToken", sessionToken))
962
- .first();
963
-
964
- if (!session) {
965
- return null;
966
- }
967
-
968
- const workspace = await ctx.db.get(session.workspaceId);
969
- if (!workspace || workspace.status !== "active") {
970
- return null;
971
- }
972
-
973
- return {
974
- workspaceId: workspace._id,
975
- email: workspace.email,
976
- tier: workspace.tier,
977
- usageCount: workspace.usageCount,
978
- usageLimit: workspace.usageLimit,
979
- };
980
- },
981
- });
982
-
983
- // Get workspace by email (for HTTP API)
984
- export const getByEmail = query({
985
- args: { email: v.string() },
986
- handler: async (ctx, { email }) => {
987
- const workspace = await ctx.db
988
- .query("workspaces")
989
- .withIndex("by_email", (q) => q.eq("email", email.toLowerCase()))
990
- .first();
991
-
992
- if (!workspace) {
993
- return null;
994
- }
995
-
996
- return {
997
- id: workspace._id,
998
- email: workspace.email,
999
- status: workspace.status,
1000
- tier: workspace.tier,
1001
- usageCount: workspace.usageCount,
1002
- usageLimit: workspace.usageLimit,
1003
- };
1004
- },
1005
- });
1006
-
1007
- // Touch session (update lastUsedAt)
1008
- export const touchSession = mutation({
1009
- args: { sessionToken: v.string() },
1010
- handler: async (ctx, { sessionToken }) => {
1011
- const session = await ctx.db
1012
- .query("agentSessions")
1013
- .withIndex("by_sessionToken", (q) => q.eq("sessionToken", sessionToken))
1014
- .first();
1015
-
1016
- if (session) {
1017
- await ctx.db.patch(session._id, { lastUsedAt: Date.now() });
1018
- }
1019
- },
1020
- });
1021
-
1022
- // ============================================
1023
- // MCP WORKSPACE FUNCTIONS
1024
- // ============================================
1025
-
1026
- // Create a new workspace (called from MCP register_owner)
1027
- export const createWorkspace = mutation({
1028
- args: { email: v.string() },
1029
- handler: async (ctx, { email }) => {
1030
- const normalizedEmail = email.toLowerCase().trim();
1031
-
1032
- // Check if workspace exists
1033
- const existing = await ctx.db
1034
- .query("workspaces")
1035
- .withIndex("by_email", (q) => q.eq("email", normalizedEmail))
1036
- .first();
1037
-
1038
- if (existing) {
1039
- return {
1040
- success: false,
1041
- error: "workspace_exists",
1042
- workspaceId: existing._id,
1043
- status: existing.status,
1044
- };
1045
- }
1046
-
1047
- // Create new workspace
1048
- const workspaceId = await ctx.db.insert("workspaces", {
1049
- email: normalizedEmail,
1050
- status: "pending",
1051
- tier: "free",
1052
- usageCount: 0,
1053
- usageLimit: 50, // Free tier limit
1054
- createdAt: Date.now(),
1055
- updatedAt: Date.now(),
1056
- });
1057
-
1058
- return { success: true, workspaceId };
1059
- },
1060
- });
1061
-
1062
- // Update workspace name
1063
- export const updateWorkspaceName = mutation({
1064
- args: {
1065
- token: v.string(),
1066
- name: v.string(),
1067
- },
1068
- handler: async (ctx, { token, name }) => {
1069
- const session = await ctx.db
1070
- .query("agentSessions")
1071
- .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
1072
- .first();
1073
- if (!session) throw new Error("Invalid session");
1074
-
1075
- const trimmed = name.trim();
1076
- if (trimmed.length < 1 || trimmed.length > 100) {
1077
- throw new Error("Name must be between 1 and 100 characters");
1078
- }
1079
-
1080
- await ctx.db.patch(session.workspaceId, {
1081
- workspaceName: trimmed,
1082
- updatedAt: Date.now(),
1083
- });
1084
-
1085
- return { success: true, name: trimmed };
1086
- },
1087
- });
1088
-
1089
- // Set or update password
1090
- export const setPassword = mutation({
1091
- args: {
1092
- token: v.string(),
1093
- password: v.string(),
1094
- },
1095
- handler: async (ctx, { token, password }) => {
1096
- const session = await ctx.db
1097
- .query("agentSessions")
1098
- .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
1099
- .first();
1100
- if (!session) throw new Error("Invalid session");
1101
-
1102
- if (password.length < 8) throw new Error("Password must be at least 8 characters");
1103
-
1104
- // Simple hash using built-in crypto
1105
- const encoder = new TextEncoder();
1106
- const data = encoder.encode(password + "apiclaw-salt-v1");
1107
- const hashBuffer = await crypto.subtle.digest("SHA-256", data);
1108
- const hashArray = Array.from(new Uint8Array(hashBuffer));
1109
- const hashHex = hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
1110
-
1111
- await ctx.db.patch(session.workspaceId, {
1112
- passwordHash: hashHex,
1113
- updatedAt: Date.now(),
1114
- });
1115
-
1116
- return { success: true };
1117
- },
1118
- });
1119
-
1120
- // Create agent session for workspace (called from MCP after verification)
1121
- export const createAgentSession = mutation({
1122
- args: {
1123
- workspaceId: v.id("workspaces"),
1124
- fingerprint: v.optional(v.string()),
1125
- },
1126
- handler: async (ctx, { workspaceId, fingerprint }) => {
1127
- const workspace = await ctx.db.get(workspaceId);
1128
- if (!workspace) {
1129
- return { success: false, error: "workspace_not_found" };
1130
- }
1131
-
1132
- if (workspace.status !== "active") {
1133
- return { success: false, error: "workspace_not_active" };
1134
- }
1135
-
1136
- const sessionToken = "apiclaw_" + generateToken();
1137
-
1138
- await ctx.db.insert("agentSessions", {
1139
- workspaceId,
1140
- sessionToken,
1141
- fingerprint,
1142
- lastUsedAt: Date.now(),
1143
- createdAt: Date.now(),
1144
- });
1145
-
1146
- return { success: true, sessionToken };
1147
- },
1148
- });
1149
-
1150
- // ============================================
1151
- // HELPER FUNCTIONS
1152
- // ============================================
1153
-
1154
- function generateToken(): string {
1155
- const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
1156
- let result = "";
1157
- for (let i = 0; i < 48; i++) {
1158
- result += chars.charAt(Math.floor(Math.random() * chars.length));
1159
- }
1160
- return result;
1161
- }
1162
-
1163
- // Get workspace status (for MCP check_workspace_status tool)
1164
- export const getWorkspaceStatus = query({
1165
- args: {
1166
- sessionToken: v.string(),
1167
- },
1168
- handler: async (ctx, args) => {
1169
- const session = await ctx.db
1170
- .query("agentSessions")
1171
- .withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.sessionToken))
1172
- .first();
1173
-
1174
- if (!session) {
1175
- return { authenticated: false };
1176
- }
1177
-
1178
- const workspace = await ctx.db.get(session.workspaceId);
1179
- if (!workspace) {
1180
- return { authenticated: false };
1181
- }
1182
-
1183
- const usageRemaining = workspace.usageLimit > 0
1184
- ? workspace.usageLimit - workspace.usageCount
1185
- : -1; // -1 = unlimited
1186
-
1187
- return {
1188
- authenticated: true,
1189
- email: workspace.email,
1190
- status: workspace.status,
1191
- tier: workspace.tier,
1192
- usageCount: workspace.usageCount,
1193
- usageLimit: workspace.usageLimit,
1194
- usageRemaining,
1195
- hasStripe: !!workspace.stripeCustomerId,
1196
- createdAt: workspace.createdAt,
1197
- };
1198
- },
1199
- });
1200
-
1201
- // Admin functions for Hivr integration
1202
- export const adminActivateWorkspace = mutation({
1203
- args: { workspaceId: v.id("workspaces") },
1204
- handler: async (ctx, { workspaceId }) => {
1205
- const workspace = await ctx.db.get(workspaceId);
1206
- if (!workspace) {
1207
- return { success: false, error: "not_found" };
1208
- }
1209
-
1210
- await ctx.db.patch(workspaceId, {
1211
- status: "active",
1212
- tier: "pro",
1213
- weeklyUsageLimit: 999999,
1214
- updatedAt: Date.now(),
1215
- });
1216
-
1217
- return { success: true };
1218
- },
1219
- });
1220
-
1221
- export const adminCreateSession = mutation({
1222
- args: { workspaceId: v.id("workspaces") },
1223
- handler: async (ctx, { workspaceId }) => {
1224
- const workspace = await ctx.db.get(workspaceId);
1225
- if (!workspace || workspace.status !== "active") {
1226
- return { success: false, error: "workspace_not_active" };
1227
- }
1228
-
1229
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
1230
- let token = '';
1231
- for (let i = 0; i < 32; i++) {
1232
- token += chars.charAt(Math.floor(Math.random() * chars.length));
1233
- }
1234
- const sessionToken = "apiclaw_" + token;
1235
-
1236
- await ctx.db.insert("agentSessions", {
1237
- workspaceId,
1238
- sessionToken,
1239
- fingerprint: "hivr-bees",
1240
- lastUsedAt: Date.now(),
1241
- createdAt: Date.now(),
1242
- });
1243
-
1244
- return { success: true, sessionToken };
1245
- },
1246
- });
1247
-
1248
- // TEMP: Admin query to debug workspace data
1249
- export const adminGetFullWorkspace = query({
1250
- args: { email: v.string() },
1251
- handler: async (ctx, { email }) => {
1252
- const workspace = await ctx.db
1253
- .query("workspaces")
1254
- .withIndex("by_email", (q) => q.eq("email", email.toLowerCase()))
1255
- .first();
1256
-
1257
- if (!workspace) {
1258
- return null;
1259
- }
1260
-
1261
- return {
1262
- _id: workspace._id,
1263
- email: workspace.email,
1264
- status: workspace.status,
1265
- tier: workspace.tier,
1266
- mainAgentId: workspace.mainAgentId || null,
1267
- mainAgentName: workspace.mainAgentName || null,
1268
- aiBackend: workspace.aiBackend || null,
1269
- usageCount: workspace.usageCount,
1270
- usageLimit: workspace.usageLimit,
1271
- createdAt: workspace.createdAt,
1272
- updatedAt: workspace.updatedAt,
1273
- };
1274
- },
1275
- });
1276
-
1277
- /**
1278
- * Claim anonymous usage history when a user registers
1279
- * Links all analytics records with matching fingerprint to the workspace
1280
- */
1281
- export const claimAnonymousUsage = mutation({
1282
- args: {
1283
- workspaceId: v.id("workspaces"),
1284
- machineFingerprint: v.string(),
1285
- },
1286
- handler: async (ctx, { workspaceId, machineFingerprint }) => {
1287
- // Verify workspace exists
1288
- const workspace = await ctx.db.get(workspaceId);
1289
- if (!workspace) {
1290
- return { success: false, error: "Workspace not found" };
1291
- }
1292
-
1293
- // Find all analytics records with matching fingerprint and no workspaceId
1294
- const analyticsRecords = await ctx.db
1295
- .query("analytics")
1296
- .withIndex("by_identifier", (q) => q.eq("identifier", machineFingerprint))
1297
- .collect();
1298
-
1299
- // Filter to only unclaimed records
1300
- const unclaimedRecords = analyticsRecords.filter((r) => !r.workspaceId);
1301
-
1302
- // Update each record to link it to the workspace
1303
- let claimedCount = 0;
1304
- for (const record of unclaimedRecords) {
1305
- await ctx.db.patch(record._id, { workspaceId });
1306
- claimedCount++;
1307
- }
1308
-
1309
- return {
1310
- success: true,
1311
- claimedCount,
1312
- message: `Claimed ${claimedCount} anonymous usage records`,
1313
- };
1314
- },
1315
- });
1316
-
1317
- export const adminUpdateEmail = mutation({
1318
- args: { workspaceId: v.id("workspaces"), newEmail: v.string() },
1319
- handler: async (ctx, { workspaceId, newEmail }) => {
1320
- await ctx.db.patch(workspaceId, { email: newEmail });
1321
- return { success: true, email: newEmail };
1322
- },
1323
- });
1324
-
1325
- export const adminSetTier = mutation({
1326
- args: { workspaceId: v.id("workspaces"), tier: v.string() },
1327
- handler: async (ctx, { workspaceId, tier }) => {
1328
- await ctx.db.patch(workspaceId, { tier, updatedAt: Date.now() });
1329
- return { success: true, tier };
1330
- },
1331
- });