@nordsym/apiclaw 1.8.7 → 1.8.8

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 (152) hide show
  1. package/README.md +58 -30
  2. package/convex/adminActivate.d.ts +3 -0
  3. package/convex/adminActivate.d.ts.map +1 -1
  4. package/convex/adminActivate.js +46 -0
  5. package/convex/adminActivate.js.map +1 -1
  6. package/convex/adminActivate.ts +1 -2
  7. package/convex/adminStats.d.ts +9 -0
  8. package/convex/adminStats.d.ts.map +1 -1
  9. package/convex/adminStats.js +282 -0
  10. package/convex/adminStats.js.map +1 -1
  11. package/convex/adminStats.ts +5 -3
  12. package/convex/agents.d.ts +84 -0
  13. package/convex/agents.js +809 -0
  14. package/convex/analytics.d.ts +5 -0
  15. package/convex/analytics.js +167 -0
  16. package/convex/apiKeys.d.ts +6 -0
  17. package/convex/apiKeys.d.ts.map +1 -0
  18. package/convex/apiKeys.js +186 -0
  19. package/convex/apiKeys.js.map +1 -0
  20. package/convex/backfillAnalytics.d.ts +2 -0
  21. package/convex/backfillAnalytics.js +20 -0
  22. package/convex/backfillSearchLogs.d.ts +2 -0
  23. package/convex/backfillSearchLogs.js +29 -0
  24. package/convex/billing.d.ts +88 -0
  25. package/convex/billing.d.ts.map +1 -1
  26. package/convex/billing.js +643 -0
  27. package/convex/billing.js.map +1 -1
  28. package/convex/billing.ts +2 -14
  29. package/convex/capabilities.d.ts +9 -0
  30. package/convex/capabilities.js +145 -0
  31. package/convex/chains.d.ts +68 -0
  32. package/convex/chains.js +1105 -0
  33. package/convex/credits.d.ts +25 -0
  34. package/convex/credits.js +186 -0
  35. package/convex/crons.d.ts +3 -0
  36. package/convex/crons.js +17 -0
  37. package/convex/debugFilestackLogs.d.ts +2 -0
  38. package/convex/debugFilestackLogs.js +17 -0
  39. package/convex/debugGetToken.d.ts +2 -0
  40. package/convex/debugGetToken.js +18 -0
  41. package/convex/directCall.d.ts +72 -0
  42. package/convex/directCall.d.ts.map +1 -1
  43. package/convex/directCall.js +663 -0
  44. package/convex/directCall.js.map +1 -1
  45. package/convex/earnProgress.d.ts +58 -0
  46. package/convex/earnProgress.js +649 -0
  47. package/convex/email.d.ts +14 -0
  48. package/convex/email.js +300 -0
  49. package/convex/email.js.map +1 -1
  50. package/convex/feedback.d.ts +7 -0
  51. package/convex/feedback.js +227 -0
  52. package/convex/http.d.ts +3 -0
  53. package/convex/http.d.ts.map +1 -1
  54. package/convex/http.js +2135 -0
  55. package/convex/http.js.map +1 -1
  56. package/convex/http.ts +275 -3
  57. package/convex/inbound.d.ts +2 -0
  58. package/convex/inbound.js +32 -0
  59. package/convex/logs.d.ts +48 -0
  60. package/convex/logs.d.ts.map +1 -1
  61. package/convex/logs.js +623 -0
  62. package/convex/logs.js.map +1 -1
  63. package/convex/migrateFilestack.d.ts +2 -0
  64. package/convex/migrateFilestack.js +74 -0
  65. package/convex/migratePartnersProd.d.ts +8 -0
  66. package/convex/migratePartnersProd.js +165 -0
  67. package/convex/migratePratham.d.ts +2 -0
  68. package/convex/migratePratham.js +121 -0
  69. package/convex/migrateProviderWorkspaces.d.ts +13 -0
  70. package/convex/migrateProviderWorkspaces.d.ts.map +1 -1
  71. package/convex/migrateProviderWorkspaces.js +141 -0
  72. package/convex/migrateProviderWorkspaces.js.map +1 -1
  73. package/convex/mou.d.ts +6 -0
  74. package/convex/mou.js +82 -0
  75. package/convex/providerKeys.d.ts +31 -0
  76. package/convex/providerKeys.js +257 -0
  77. package/convex/providers.d.ts +35 -0
  78. package/convex/providers.d.ts.map +1 -1
  79. package/convex/providers.js +1027 -0
  80. package/convex/providers.js.map +1 -1
  81. package/convex/purchases.d.ts +7 -0
  82. package/convex/purchases.js +157 -0
  83. package/convex/ratelimit.d.ts +4 -0
  84. package/convex/ratelimit.js +91 -0
  85. package/convex/schema.ts +4 -4
  86. package/convex/searchLogs.d.ts +13 -0
  87. package/convex/searchLogs.d.ts.map +1 -1
  88. package/convex/searchLogs.js +241 -0
  89. package/convex/searchLogs.js.map +1 -1
  90. package/convex/seedAPILayerAPIs.d.ts +7 -0
  91. package/convex/seedAPILayerAPIs.js +177 -0
  92. package/convex/seedDirectCallConfigs.d.ts +2 -0
  93. package/convex/seedDirectCallConfigs.js +324 -0
  94. package/convex/seedPratham.d.ts +6 -0
  95. package/convex/seedPratham.d.ts.map +1 -1
  96. package/convex/seedPratham.js +149 -0
  97. package/convex/seedPratham.js.map +1 -1
  98. package/convex/seedPratham.ts +1 -2
  99. package/convex/spendAlerts.d.ts +36 -0
  100. package/convex/spendAlerts.js +380 -0
  101. package/convex/spendAlerts.js.map +1 -1
  102. package/convex/stripeActions.d.ts +19 -0
  103. package/convex/stripeActions.d.ts.map +1 -1
  104. package/convex/stripeActions.js +432 -0
  105. package/convex/stripeActions.js.map +1 -1
  106. package/convex/stripeActions.ts +25 -3
  107. package/convex/teams.d.ts +21 -0
  108. package/convex/teams.js +215 -0
  109. package/convex/telemetry.d.ts +4 -0
  110. package/convex/telemetry.js +74 -0
  111. package/convex/updateAPIStatus.d.ts +6 -0
  112. package/convex/updateAPIStatus.d.ts.map +1 -1
  113. package/convex/updateAPIStatus.js +39 -0
  114. package/convex/updateAPIStatus.js.map +1 -1
  115. package/convex/usage.d.ts +27 -0
  116. package/convex/usage.js +229 -0
  117. package/convex/waitlist.d.ts +4 -0
  118. package/convex/waitlist.js +49 -0
  119. package/convex/webhooks.d.ts +12 -0
  120. package/convex/webhooks.js +410 -0
  121. package/convex/workspaceSettings.d.ts +7 -0
  122. package/convex/workspaceSettings.d.ts.map +1 -0
  123. package/convex/workspaceSettings.js +128 -0
  124. package/convex/workspaceSettings.js.map +1 -0
  125. package/convex/workspaces.d.ts +33 -0
  126. package/convex/workspaces.d.ts.map +1 -1
  127. package/convex/workspaces.js +989 -0
  128. package/convex/workspaces.js.map +1 -1
  129. package/convex/workspaces.ts +18 -20
  130. package/dist/bin.js +0 -0
  131. package/dist/cli/commands/demo.js +1 -1
  132. package/dist/cli/commands/demo.js.map +1 -1
  133. package/dist/cli/commands/doctor.js.map +1 -1
  134. package/dist/cli/commands/login.js.map +1 -1
  135. package/dist/cli/commands/setup.js.map +1 -1
  136. package/dist/credentials.d.ts.map +1 -1
  137. package/dist/credentials.js +15 -0
  138. package/dist/credentials.js.map +1 -1
  139. package/dist/discovery.js.map +1 -1
  140. package/dist/execute.js.map +1 -1
  141. package/dist/index.js +1 -1
  142. package/dist/index.js.map +1 -1
  143. package/dist/open-apis.d.ts.map +1 -1
  144. package/dist/open-apis.js +94 -2
  145. package/dist/open-apis.js.map +1 -1
  146. package/dist/ui/errors.js.map +1 -1
  147. package/dist/ui/prompts.js.map +1 -1
  148. package/package.json +1 -1
  149. package/src/cli/commands/demo.ts +1 -1
  150. package/src/credentials.ts +16 -0
  151. package/src/index.ts +1 -1
  152. package/src/open-apis.ts +114 -4
@@ -0,0 +1,1027 @@
1
+ import { mutation, query } from "./_generated/server";
2
+ import { v } from "convex/values";
3
+ // Register a new provider and their first API
4
+ export const registerProvider = mutation({
5
+ args: {
6
+ provider: v.object({
7
+ name: v.string(),
8
+ email: v.string(),
9
+ website: v.optional(v.string()),
10
+ }),
11
+ api: v.object({
12
+ name: v.string(),
13
+ description: v.string(),
14
+ category: v.string(),
15
+ openApiUrl: v.optional(v.string()),
16
+ docsUrl: v.optional(v.string()),
17
+ pricingModel: v.string(),
18
+ pricingNotes: v.optional(v.string()),
19
+ }),
20
+ },
21
+ handler: async (ctx, args) => {
22
+ const now = Date.now();
23
+ // Check if provider already exists by email
24
+ const existing = await ctx.db
25
+ .query("providers")
26
+ .withIndex("by_email", (q) => q.eq("email", args.provider.email))
27
+ .first();
28
+ let providerId;
29
+ if (existing) {
30
+ // Use existing provider
31
+ providerId = existing._id;
32
+ }
33
+ else {
34
+ // Create new provider - auto-approve for now
35
+ providerId = await ctx.db.insert("providers", {
36
+ name: args.provider.name,
37
+ email: args.provider.email,
38
+ website: args.provider.website,
39
+ status: "approved", // Auto-approve for MVP
40
+ createdAt: now,
41
+ updatedAt: now,
42
+ approvedAt: now,
43
+ });
44
+ }
45
+ // Create the API listing - auto-approve for now
46
+ const apiId = await ctx.db.insert("providerAPIs", {
47
+ providerId,
48
+ name: args.api.name,
49
+ description: args.api.description,
50
+ category: args.api.category,
51
+ openApiUrl: args.api.openApiUrl,
52
+ docsUrl: args.api.docsUrl,
53
+ pricingModel: args.api.pricingModel,
54
+ pricingNotes: args.api.pricingNotes,
55
+ status: "approved", // Auto-approve for MVP
56
+ createdAt: now,
57
+ approvedAt: now,
58
+ discoveryCount: 0,
59
+ });
60
+ // Find or create workspace for this provider email
61
+ const emailLower = args.provider.email.toLowerCase();
62
+ let workspace = await ctx.db
63
+ .query("workspaces")
64
+ .withIndex("by_email", (q) => q.eq("email", emailLower))
65
+ .first();
66
+ if (!workspace) {
67
+ const wsId = await ctx.db.insert("workspaces", {
68
+ email: emailLower,
69
+ status: "active",
70
+ tier: "free",
71
+ usageCount: 0,
72
+ usageLimit: 50,
73
+ weeklyUsageCount: 0,
74
+ weeklyUsageLimit: 50,
75
+ hourlyUsageCount: 0,
76
+ createdAt: now,
77
+ updatedAt: now,
78
+ });
79
+ workspace = await ctx.db.get(wsId);
80
+ }
81
+ // Link provider → workspace (if not already)
82
+ const provider = await ctx.db.get(providerId);
83
+ if (provider && !provider.workspaceId) {
84
+ await ctx.db.patch(providerId, { workspaceId: workspace._id });
85
+ }
86
+ // Create unified session (agentSessions only - legacy sessions table deprecated)
87
+ const sessionToken = generateToken();
88
+ await ctx.db.insert("agentSessions", {
89
+ workspaceId: workspace._id,
90
+ sessionToken,
91
+ lastUsedAt: now,
92
+ createdAt: now,
93
+ });
94
+ return { providerId, apiId, sessionToken };
95
+ },
96
+ });
97
+ // Get provider by email
98
+ export const getProviderByEmail = query({
99
+ args: { email: v.string() },
100
+ handler: async (ctx, args) => {
101
+ return await ctx.db
102
+ .query("providers")
103
+ .withIndex("by_email", (q) => q.eq("email", args.email))
104
+ .first();
105
+ },
106
+ });
107
+ // Get all APIs for a provider
108
+ export const getProviderAPIs = query({
109
+ args: { providerId: v.id("providers") },
110
+ handler: async (ctx, args) => {
111
+ return await ctx.db
112
+ .query("providerAPIs")
113
+ .withIndex("by_providerId", (q) => q.eq("providerId", args.providerId))
114
+ .collect();
115
+ },
116
+ });
117
+ // Get all approved APIs (for the registry)
118
+ export const getApprovedAPIs = query({
119
+ args: {
120
+ category: v.optional(v.string()),
121
+ limit: v.optional(v.number()),
122
+ },
123
+ handler: async (ctx, args) => {
124
+ const query = ctx.db
125
+ .query("providerAPIs")
126
+ .withIndex("by_status", (q) => q.eq("status", "approved"));
127
+ const apis = await query.collect();
128
+ // Filter by category if provided
129
+ let filtered = args.category
130
+ ? apis.filter((api) => api.category === args.category)
131
+ : apis;
132
+ // Apply limit
133
+ if (args.limit) {
134
+ filtered = filtered.slice(0, args.limit);
135
+ }
136
+ return filtered;
137
+ },
138
+ });
139
+ // Get API categories with counts
140
+ export const getCategories = query({
141
+ handler: async (ctx) => {
142
+ const apis = await ctx.db
143
+ .query("providerAPIs")
144
+ .withIndex("by_status", (q) => q.eq("status", "approved"))
145
+ .collect();
146
+ const categories = {};
147
+ for (const api of apis) {
148
+ categories[api.category] = (categories[api.category] || 0) + 1;
149
+ }
150
+ return Object.entries(categories)
151
+ .map(([name, count]) => ({ name, count }))
152
+ .sort((a, b) => b.count - a.count);
153
+ },
154
+ });
155
+ // Increment discovery count when an agent finds an API
156
+ export const trackDiscovery = mutation({
157
+ args: { apiId: v.id("providerAPIs") },
158
+ handler: async (ctx, args) => {
159
+ const api = await ctx.db.get(args.apiId);
160
+ if (!api)
161
+ return;
162
+ await ctx.db.patch(args.apiId, {
163
+ discoveryCount: (api.discoveryCount || 0) + 1,
164
+ lastDiscoveredAt: Date.now(),
165
+ });
166
+ },
167
+ });
168
+ // Unified discovery logging
169
+ // Single source of truth: apiLogs. discoveryCount on My APIs is derived from apiLogs.
170
+ export const logDiscovery = mutation({
171
+ args: {
172
+ provider: v.string(),
173
+ query: v.string(),
174
+ latencyMs: v.number(),
175
+ callerWorkspaceId: v.string(),
176
+ },
177
+ handler: async (ctx, args) => {
178
+ // Resolve provider → workspace dynamically (no hardcoded email maps)
179
+ const providerNameLower = args.provider.toLowerCase();
180
+ const allProviders = await ctx.db.query("providers").collect();
181
+ const providerRecord = allProviders.find((p) => p.name.toLowerCase() === providerNameLower);
182
+ if (!providerRecord || !providerRecord.workspaceId)
183
+ return { logged: false };
184
+ const workspace = await ctx.db.get(providerRecord.workspaceId);
185
+ if (!workspace)
186
+ return { logged: false };
187
+ const wsId = workspace._id;
188
+ // 1. Log to apiLogs (source of truth for Analytics)
189
+ await ctx.db.insert("apiLogs", {
190
+ workspaceId: wsId,
191
+ sessionToken: "",
192
+ provider: args.provider,
193
+ action: `discovery:${args.query}`,
194
+ status: "success",
195
+ latencyMs: args.latencyMs,
196
+ direction: "inbound",
197
+ callerWorkspaceId: args.callerWorkspaceId,
198
+ createdAt: Date.now(),
199
+ });
200
+ // 2. Increment discoveryCount on MATCHING APIs only
201
+ const apis = await ctx.db
202
+ .query("providerAPIs")
203
+ .withIndex("by_providerId", (q) => q.eq("providerId", providerRecord._id))
204
+ .collect();
205
+ const queryLower = args.query.toLowerCase();
206
+ const queryWords = queryLower.split(/\s+/).filter((w) => w.length > 2);
207
+ let matched = 0;
208
+ for (const api of apis) {
209
+ const apiText = `${api.name} ${api.description || ""}`.toLowerCase();
210
+ if (queryWords.some((w) => apiText.includes(w))) {
211
+ await ctx.db.patch(api._id, {
212
+ discoveryCount: (api.discoveryCount || 0) + 1,
213
+ lastDiscoveredAt: Date.now(),
214
+ });
215
+ matched++;
216
+ }
217
+ }
218
+ return { logged: true, matched };
219
+ },
220
+ });
221
+ // Legacy: Track discovery by provider name (kept for backwards compat)
222
+ export const trackDiscoveryByProvider = mutation({
223
+ args: { provider: v.string(), query: v.string() },
224
+ handler: async (ctx, args) => {
225
+ // Resolve provider dynamically by name (no hardcoded email maps)
226
+ const providerNameLower = args.provider.toLowerCase();
227
+ const allProviders = await ctx.db.query("providers").collect();
228
+ const provider = allProviders.find((p) => p.name.toLowerCase() === providerNameLower);
229
+ if (!provider)
230
+ return { updated: 0, error: "provider not found" };
231
+ // Get all APIs for this provider
232
+ const providerApis = await ctx.db
233
+ .query("providerAPIs")
234
+ .withIndex("by_providerId", (q) => q.eq("providerId", provider._id))
235
+ .collect();
236
+ // Increment discoveryCount on ALL provider APIs
237
+ for (const api of providerApis) {
238
+ await ctx.db.patch(api._id, {
239
+ discoveryCount: (api.discoveryCount || 0) + 1,
240
+ lastDiscoveredAt: Date.now(),
241
+ });
242
+ }
243
+ return { updated: providerApis.length };
244
+ },
245
+ });
246
+ // Admin: List pending providers
247
+ export const getPendingProviders = query({
248
+ handler: async (ctx) => {
249
+ return await ctx.db
250
+ .query("providers")
251
+ .withIndex("by_status", (q) => q.eq("status", "pending"))
252
+ .collect();
253
+ },
254
+ });
255
+ // Admin: Approve provider
256
+ export const approveProvider = mutation({
257
+ args: { providerId: v.id("providers") },
258
+ handler: async (ctx, args) => {
259
+ await ctx.db.patch(args.providerId, {
260
+ status: "approved",
261
+ approvedAt: Date.now(),
262
+ updatedAt: Date.now(),
263
+ });
264
+ },
265
+ });
266
+ // Admin: Reject provider
267
+ export const rejectProvider = mutation({
268
+ args: { providerId: v.id("providers") },
269
+ handler: async (ctx, args) => {
270
+ await ctx.db.patch(args.providerId, {
271
+ status: "rejected",
272
+ updatedAt: Date.now(),
273
+ });
274
+ },
275
+ });
276
+ // Get provider stats
277
+ export const getProviderStats = query({
278
+ handler: async (ctx) => {
279
+ const providers = await ctx.db.query("providers").collect();
280
+ const apis = await ctx.db.query("providerAPIs").collect();
281
+ return {
282
+ totalProviders: providers.length,
283
+ approvedProviders: providers.filter((p) => p.status === "approved").length,
284
+ pendingProviders: providers.filter((p) => p.status === "pending").length,
285
+ totalAPIs: apis.length,
286
+ approvedAPIs: apis.filter((a) => a.status === "approved").length,
287
+ pendingAPIs: apis.filter((a) => a.status === "pending").length,
288
+ totalDiscoveries: apis.reduce((sum, a) => sum + (a.discoveryCount || 0), 0),
289
+ };
290
+ },
291
+ });
292
+ // ============================================
293
+ // DASHBOARD AUTH & SESSION FUNCTIONS
294
+ // ============================================
295
+ // Create magic link for email auth (unified: writes to workspaceMagicLinks)
296
+ export const createMagicLink = mutation({
297
+ args: { email: v.string() },
298
+ handler: async (ctx, { email }) => {
299
+ const token = generateToken();
300
+ const expiresAt = Date.now() + 15 * 60 * 1000; // 15 minutes
301
+ await ctx.db.insert("workspaceMagicLinks", {
302
+ email: email.toLowerCase(),
303
+ token,
304
+ expiresAt,
305
+ createdAt: Date.now(),
306
+ });
307
+ return { token, expiresAt };
308
+ },
309
+ });
310
+ // Verify magic link and create unified session (workspace + provider)
311
+ export const verifyMagicLink = mutation({
312
+ args: { token: v.string() },
313
+ handler: async (ctx, { token }) => {
314
+ const magicLink = await ctx.db
315
+ .query("workspaceMagicLinks")
316
+ .withIndex("by_token", (q) => q.eq("token", token))
317
+ .first();
318
+ if (!magicLink) {
319
+ return { success: false, error: "Invalid token" };
320
+ }
321
+ if (magicLink.expiresAt < Date.now()) {
322
+ return { success: false, error: "Token expired" };
323
+ }
324
+ if (magicLink.usedAt) {
325
+ return { success: false, error: "Token already used" };
326
+ }
327
+ // Mark as used
328
+ await ctx.db.patch(magicLink._id, { usedAt: Date.now() });
329
+ const now = Date.now();
330
+ // Find or create workspace
331
+ let workspace = await ctx.db
332
+ .query("workspaces")
333
+ .withIndex("by_email", (q) => q.eq("email", magicLink.email))
334
+ .first();
335
+ if (!workspace) {
336
+ const wsId = await ctx.db.insert("workspaces", {
337
+ email: magicLink.email,
338
+ status: "active",
339
+ tier: "free",
340
+ usageCount: 0,
341
+ usageLimit: 50,
342
+ weeklyUsageCount: 0,
343
+ weeklyUsageLimit: 50,
344
+ hourlyUsageCount: 0,
345
+ createdAt: now,
346
+ updatedAt: now,
347
+ });
348
+ workspace = await ctx.db.get(wsId);
349
+ }
350
+ // Find or create provider
351
+ let provider = await ctx.db
352
+ .query("providers")
353
+ .withIndex("by_email", (q) => q.eq("email", magicLink.email))
354
+ .first();
355
+ if (!provider) {
356
+ const providerId = await ctx.db.insert("providers", {
357
+ email: magicLink.email,
358
+ name: magicLink.email.split("@")[0],
359
+ status: "approved",
360
+ workspaceId: workspace._id,
361
+ createdAt: now,
362
+ updatedAt: now,
363
+ });
364
+ provider = await ctx.db.get(providerId);
365
+ }
366
+ else if (!provider.workspaceId) {
367
+ // Link existing provider to workspace
368
+ await ctx.db.patch(provider._id, { workspaceId: workspace._id });
369
+ }
370
+ // Create unified session (agentSessions)
371
+ const sessionToken = generateToken();
372
+ await ctx.db.insert("agentSessions", {
373
+ workspaceId: workspace._id,
374
+ sessionToken,
375
+ lastUsedAt: now,
376
+ createdAt: now,
377
+ });
378
+ return {
379
+ success: true,
380
+ sessionToken,
381
+ provider: {
382
+ id: provider._id,
383
+ email: provider.email,
384
+ name: provider.name,
385
+ },
386
+ };
387
+ },
388
+ });
389
+ // Get current session (unified: tries agentSessions first, falls back to legacy sessions)
390
+ export const getSession = query({
391
+ args: { token: v.string() },
392
+ handler: async (ctx, { token }) => {
393
+ // 1. Try unified agentSessions (by sessionToken)
394
+ const agentSession = await ctx.db
395
+ .query("agentSessions")
396
+ .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
397
+ .first();
398
+ if (agentSession) {
399
+ // Resolve provider via workspace
400
+ const provider = await ctx.db
401
+ .query("providers")
402
+ .withIndex("by_workspaceId", (q) => q.eq("workspaceId", agentSession.workspaceId))
403
+ .first();
404
+ if (!provider)
405
+ return null;
406
+ return {
407
+ providerId: provider._id,
408
+ email: provider.email,
409
+ name: provider.name,
410
+ stripeOnboardingComplete: provider.stripeOnboardingComplete,
411
+ };
412
+ }
413
+ // 2. Fallback: legacy sessions table (for tokens created before migration)
414
+ const session = await ctx.db
415
+ .query("sessions")
416
+ .withIndex("by_token", (q) => q.eq("token", token))
417
+ .first();
418
+ if (!session || session.expiresAt < Date.now()) {
419
+ return null;
420
+ }
421
+ const provider = await ctx.db.get(session.providerId);
422
+ if (!provider)
423
+ return null;
424
+ return {
425
+ providerId: provider._id,
426
+ email: provider.email,
427
+ name: provider.name,
428
+ stripeOnboardingComplete: provider.stripeOnboardingComplete,
429
+ };
430
+ },
431
+ });
432
+ // ============================================
433
+ // DASHBOARD ANALYTICS
434
+ // ============================================
435
+ // Get single API by ID
436
+ export const getApiById = query({
437
+ args: { apiId: v.string() },
438
+ handler: async (ctx, args) => {
439
+ // Try to get by document ID
440
+ try {
441
+ const api = await ctx.db.get(args.apiId);
442
+ if (api) {
443
+ // Check if it has Direct Call configured
444
+ const directCall = await ctx.db
445
+ .query("providerDirectCall")
446
+ .filter((q) => q.eq(q.field("apiId"), args.apiId))
447
+ .first();
448
+ return { ...api, hasDirectCall: !!directCall, directCallStatus: directCall?.status };
449
+ }
450
+ }
451
+ catch {
452
+ // Not a valid ID format
453
+ }
454
+ return null;
455
+ },
456
+ });
457
+ // Get provider APIs with Direct Call status
458
+ export const getProviderAPIsWithStatus = query({
459
+ args: { providerId: v.string() },
460
+ handler: async (ctx, args) => {
461
+ const apis = await ctx.db
462
+ .query("providerAPIs")
463
+ .filter((q) => q.eq(q.field("providerId"), args.providerId))
464
+ .collect();
465
+ // Add Direct Call status to each API
466
+ const apisWithStatus = await Promise.all(apis.map(async (api) => {
467
+ const directCall = await ctx.db
468
+ .query("providerDirectCall")
469
+ .filter((q) => q.eq(q.field("apiId"), api._id))
470
+ .first();
471
+ return {
472
+ ...api,
473
+ hasDirectCall: !!directCall,
474
+ directCallStatus: directCall?.status,
475
+ };
476
+ }));
477
+ return apisWithStatus;
478
+ },
479
+ });
480
+ // DEBUG: Delete API
481
+ export const debugDeleteAPI = mutation({
482
+ args: { apiId: v.string() },
483
+ handler: async (ctx, args) => {
484
+ await ctx.db.delete(args.apiId);
485
+ return { deleted: true };
486
+ },
487
+ });
488
+ // Add API for logged-in provider (used by register page)
489
+ export const addAPI = mutation({
490
+ args: {
491
+ token: v.string(),
492
+ api: v.object({
493
+ name: v.string(),
494
+ description: v.string(),
495
+ category: v.string(),
496
+ openApiUrl: v.optional(v.string()),
497
+ docsUrl: v.optional(v.string()),
498
+ pricingModel: v.string(),
499
+ pricingNotes: v.optional(v.string()),
500
+ }),
501
+ },
502
+ handler: async (ctx, args) => {
503
+ // Unified session lookup
504
+ let providerId = null;
505
+ const agentSession = await ctx.db
506
+ .query("agentSessions")
507
+ .withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.token))
508
+ .first();
509
+ if (agentSession) {
510
+ const prov = await ctx.db
511
+ .query("providers")
512
+ .withIndex("by_workspaceId", (q) => q.eq("workspaceId", agentSession.workspaceId))
513
+ .first();
514
+ if (prov)
515
+ providerId = prov._id;
516
+ }
517
+ else {
518
+ const session = await ctx.db
519
+ .query("sessions")
520
+ .withIndex("by_token", (q) => q.eq("token", args.token))
521
+ .first();
522
+ if (session && session.expiresAt >= Date.now()) {
523
+ providerId = session.providerId;
524
+ }
525
+ }
526
+ if (!providerId)
527
+ throw new Error("Invalid or expired session");
528
+ const now = Date.now();
529
+ const apiId = await ctx.db.insert("providerAPIs", {
530
+ providerId,
531
+ name: args.api.name,
532
+ description: args.api.description,
533
+ category: args.api.category,
534
+ openApiUrl: args.api.openApiUrl,
535
+ docsUrl: args.api.docsUrl,
536
+ pricingModel: args.api.pricingModel,
537
+ pricingNotes: args.api.pricingNotes,
538
+ status: "approved",
539
+ createdAt: now,
540
+ approvedAt: now,
541
+ discoveryCount: 0,
542
+ });
543
+ return { apiId, success: true };
544
+ },
545
+ });
546
+ // Delete API for logged-in provider
547
+ export const deleteAPI = mutation({
548
+ args: {
549
+ token: v.string(),
550
+ apiId: v.string(),
551
+ },
552
+ handler: async (ctx, args) => {
553
+ // Unified session lookup
554
+ let providerId = null;
555
+ const agentSession = await ctx.db
556
+ .query("agentSessions")
557
+ .withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.token))
558
+ .first();
559
+ if (agentSession) {
560
+ const prov = await ctx.db
561
+ .query("providers")
562
+ .withIndex("by_workspaceId", (q) => q.eq("workspaceId", agentSession.workspaceId))
563
+ .first();
564
+ if (prov)
565
+ providerId = prov._id;
566
+ }
567
+ else {
568
+ const session = await ctx.db
569
+ .query("sessions")
570
+ .withIndex("by_token", (q) => q.eq("token", args.token))
571
+ .first();
572
+ if (session && session.expiresAt >= Date.now()) {
573
+ providerId = session.providerId;
574
+ }
575
+ }
576
+ if (!providerId)
577
+ throw new Error("Invalid or expired session");
578
+ // Get the API and verify ownership
579
+ const api = await ctx.db.get(args.apiId);
580
+ if (!api || api.providerId !== providerId) {
581
+ throw new Error("API not found or unauthorized");
582
+ }
583
+ // Delete the API
584
+ await ctx.db.delete(args.apiId);
585
+ // Also delete any Direct Call config
586
+ const directCallConfig = await ctx.db
587
+ .query("providerDirectCall")
588
+ .filter((q) => q.eq(q.field("apiId"), args.apiId))
589
+ .first();
590
+ if (directCallConfig) {
591
+ await ctx.db.delete(directCallConfig._id);
592
+ }
593
+ return { deleted: true };
594
+ },
595
+ });
596
+ // DEBUG: Update provider name
597
+ export const debugUpdateProvider = mutation({
598
+ args: {
599
+ providerId: v.string(),
600
+ name: v.optional(v.string()),
601
+ },
602
+ handler: async (ctx, args) => {
603
+ const updates = {};
604
+ if (args.name)
605
+ updates.name = args.name;
606
+ await ctx.db.patch(args.providerId, updates);
607
+ return { updated: true };
608
+ },
609
+ });
610
+ // DEBUG: Add API for provider (seeding)
611
+ export const debugAddAPI = mutation({
612
+ args: {
613
+ providerId: v.string(),
614
+ name: v.string(),
615
+ description: v.string(),
616
+ category: v.string(),
617
+ docsUrl: v.optional(v.string()),
618
+ pricingModel: v.string(),
619
+ pricingNotes: v.optional(v.string()),
620
+ },
621
+ handler: async (ctx, args) => {
622
+ const now = Date.now();
623
+ return await ctx.db.insert("providerAPIs", {
624
+ providerId: args.providerId,
625
+ name: args.name,
626
+ description: args.description,
627
+ category: args.category,
628
+ docsUrl: args.docsUrl,
629
+ pricingModel: args.pricingModel,
630
+ pricingNotes: args.pricingNotes,
631
+ status: "approved",
632
+ createdAt: now,
633
+ approvedAt: now,
634
+ discoveryCount: 0,
635
+ });
636
+ },
637
+ });
638
+ // DEBUG: Delete provider and all related data
639
+ export const debugDeleteProvider = mutation({
640
+ args: { providerId: v.string() },
641
+ handler: async (ctx, args) => {
642
+ const providerId = args.providerId;
643
+ // Delete sessions
644
+ const sessions = await ctx.db.query("sessions").filter(q => q.eq(q.field("providerId"), providerId)).collect();
645
+ for (const s of sessions)
646
+ await ctx.db.delete(s._id);
647
+ // Delete APIs
648
+ const apis = await ctx.db.query("providerAPIs").filter(q => q.eq(q.field("providerId"), providerId)).collect();
649
+ for (const a of apis)
650
+ await ctx.db.delete(a._id);
651
+ // Delete direct call configs
652
+ const configs = await ctx.db.query("providerDirectCall").filter(q => q.eq(q.field("providerId"), providerId)).collect();
653
+ for (const c of configs) {
654
+ // Delete actions for this config
655
+ const actions = await ctx.db.query("providerActions").filter(q => q.eq(q.field("directCallId"), c._id)).collect();
656
+ for (const act of actions)
657
+ await ctx.db.delete(act._id);
658
+ await ctx.db.delete(c._id);
659
+ }
660
+ // Delete provider
661
+ await ctx.db.delete(providerId);
662
+ return { deleted: true };
663
+ },
664
+ });
665
+ // DEBUG: List all sessions
666
+ export const debugListSessions = query({
667
+ args: {},
668
+ handler: async (ctx) => {
669
+ return await ctx.db.query("sessions").collect();
670
+ },
671
+ });
672
+ // DEBUG: List all providers
673
+ export const debugListProviders = query({
674
+ args: {},
675
+ handler: async (ctx) => {
676
+ return await ctx.db.query("providers").collect();
677
+ },
678
+ });
679
+ export const getAnalytics = query({
680
+ args: {
681
+ token: v.optional(v.string()),
682
+ workspaceId: v.optional(v.string()), // Direct workspace ID (used by /workspace page)
683
+ period: v.optional(v.string()), // "week", "month", "all"
684
+ },
685
+ handler: async (ctx, { token, workspaceId: wsIdArg, period = "month" }) => {
686
+ let providerId = null;
687
+ // Path 1: Direct workspaceId (from workspace page)
688
+ if (wsIdArg) {
689
+ const prov = await ctx.db
690
+ .query("providers")
691
+ .withIndex("by_workspaceId", (q) => q.eq("workspaceId", wsIdArg))
692
+ .first();
693
+ if (prov)
694
+ providerId = prov._id;
695
+ }
696
+ // Path 2: Session token lookup (from provider dashboard / API)
697
+ if (!providerId && token) {
698
+ const agentSession = await ctx.db
699
+ .query("agentSessions")
700
+ .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
701
+ .first();
702
+ if (agentSession) {
703
+ const prov = await ctx.db
704
+ .query("providers")
705
+ .withIndex("by_workspaceId", (q) => q.eq("workspaceId", agentSession.workspaceId))
706
+ .first();
707
+ if (prov)
708
+ providerId = prov._id;
709
+ }
710
+ else {
711
+ const session = await ctx.db
712
+ .query("sessions")
713
+ .withIndex("by_token", (q) => q.eq("token", token))
714
+ .first();
715
+ if (session && session.expiresAt >= Date.now()) {
716
+ providerId = session.providerId;
717
+ }
718
+ }
719
+ }
720
+ if (!providerId)
721
+ return null;
722
+ const provider = await ctx.db.get(providerId);
723
+ if (!provider)
724
+ return null;
725
+ const now = Date.now();
726
+ const periodMs = {
727
+ week: 7 * 24 * 60 * 60 * 1000,
728
+ month: 30 * 24 * 60 * 60 * 1000,
729
+ all: now,
730
+ }[period] || 30 * 24 * 60 * 60 * 1000;
731
+ const startTime = now - periodMs;
732
+ // Provider name key used in apiLogs.provider (lowercase)
733
+ const providerKey = provider.name.toLowerCase();
734
+ // Get real data from apiLogs (source of truth for all API activity)
735
+ const allLogs = await ctx.db
736
+ .query("apiLogs")
737
+ .withIndex("by_provider", (q) => q.eq("provider", providerKey))
738
+ .collect();
739
+ const periodLogs = allLogs.filter((l) => l.createdAt >= startTime);
740
+ // Split into direct calls vs discovery
741
+ const directCalls = periodLogs.filter((l) => !l.action?.startsWith("discovery:"));
742
+ const discoveries = periodLogs.filter((l) => l.action?.startsWith("discovery:"));
743
+ // Calculate metrics
744
+ const totalCalls = directCalls.length;
745
+ const totalDiscoveries = discoveries.length;
746
+ const uniqueCallers = new Set(periodLogs.map((l) => l.callerWorkspaceId || l.workspaceId)).size;
747
+ const successCount = directCalls.filter((l) => l.status === "success").length;
748
+ const successRate = totalCalls > 0 ? (successCount / totalCalls) * 100 : 100;
749
+ const avgLatency = totalCalls > 0
750
+ ? Math.round(directCalls.reduce((sum, l) => sum + l.latencyMs, 0) / totalCalls)
751
+ : 0;
752
+ // Calls over time (daily buckets)
753
+ const callsByDay = {};
754
+ periodLogs.forEach((log) => {
755
+ const day = new Date(log.createdAt).toISOString().split("T")[0];
756
+ if (!callsByDay[day]) {
757
+ callsByDay[day] = { calls: 0, discoveries: 0, success: 0 };
758
+ }
759
+ if (log.action?.startsWith("discovery:")) {
760
+ callsByDay[day].discoveries += 1;
761
+ }
762
+ else {
763
+ callsByDay[day].calls += 1;
764
+ if (log.status === "success")
765
+ callsByDay[day].success += 1;
766
+ }
767
+ });
768
+ // Top actions
769
+ const actionCallCounts = {};
770
+ directCalls.forEach((log) => {
771
+ const action = log.action || "unknown";
772
+ actionCallCounts[action] = (actionCallCounts[action] || 0) + 1;
773
+ });
774
+ const topActions = Object.entries(actionCallCounts)
775
+ .sort((a, b) => b[1] - a[1])
776
+ .slice(0, 10)
777
+ .map(([actionName, calls]) => ({ actionName, calls }));
778
+ // Top callers (workspace IDs that called this provider)
779
+ const callerCounts = {};
780
+ directCalls.forEach((log) => {
781
+ const caller = log.callerWorkspaceId || "anonymous";
782
+ callerCounts[caller] = (callerCounts[caller] || 0) + 1;
783
+ });
784
+ const topAgents = Object.entries(callerCounts)
785
+ .sort((a, b) => b[1] - a[1])
786
+ .slice(0, 10)
787
+ .map(([agentId, calls]) => ({ agentId, calls }));
788
+ // Get provider's APIs
789
+ const apis = await ctx.db
790
+ .query("providerAPIs")
791
+ .withIndex("by_providerId", (q) => q.eq("providerId", providerId))
792
+ .collect();
793
+ // Per-API call counts (match action name to API name)
794
+ const apiCallCounts = {};
795
+ const apiDiscoveryCounts = {};
796
+ for (const api of apis) {
797
+ const apiNameLower = api.name.toLowerCase();
798
+ apiCallCounts[api._id] = directCalls.filter((l) => l.action?.toLowerCase().includes(apiNameLower)).length;
799
+ apiDiscoveryCounts[api._id] = discoveries.filter((l) => l.action?.toLowerCase().includes(apiNameLower)).length;
800
+ }
801
+ return {
802
+ totalCalls,
803
+ totalDiscoveries,
804
+ uniqueAgents: uniqueCallers,
805
+ totalRevenue: 0, // Revenue tracking not yet implemented
806
+ successRate: Math.round(successRate * 10) / 10,
807
+ avgLatency,
808
+ callsByDay: Object.entries(callsByDay)
809
+ .map(([date, data]) => ({
810
+ date,
811
+ calls: data.calls,
812
+ discoveries: data.discoveries,
813
+ revenue: 0,
814
+ }))
815
+ .sort((a, b) => a.date.localeCompare(b.date)),
816
+ topAgents,
817
+ topActions,
818
+ apis: apis.map((api) => ({
819
+ id: api._id,
820
+ name: api.name,
821
+ calls: apiCallCounts[api._id] || 0,
822
+ discoveries: apiDiscoveryCounts[api._id] || 0,
823
+ status: api.status,
824
+ })),
825
+ isPreview: false,
826
+ };
827
+ },
828
+ });
829
+ // ============================================
830
+ // DASHBOARD EARNINGS
831
+ // ============================================
832
+ // Earnings placeholder - partners (APILayer, Filestack) don't earn per-call revenue yet
833
+ export const getEarnings = query({
834
+ args: { token: v.string() },
835
+ handler: async (ctx, { token }) => {
836
+ // Unified session lookup
837
+ let providerId = null;
838
+ const agentSession = await ctx.db
839
+ .query("agentSessions")
840
+ .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
841
+ .first();
842
+ if (agentSession) {
843
+ const prov = await ctx.db
844
+ .query("providers")
845
+ .withIndex("by_workspaceId", (q) => q.eq("workspaceId", agentSession.workspaceId))
846
+ .first();
847
+ if (prov)
848
+ providerId = prov._id;
849
+ }
850
+ else {
851
+ const session = await ctx.db
852
+ .query("sessions")
853
+ .withIndex("by_token", (q) => q.eq("token", token))
854
+ .first();
855
+ if (session && session.expiresAt >= Date.now()) {
856
+ providerId = session.providerId;
857
+ }
858
+ }
859
+ if (!providerId)
860
+ return null;
861
+ // Get all payouts (currently empty for all providers)
862
+ const payouts = await ctx.db
863
+ .query("payouts")
864
+ .withIndex("by_providerId", (q) => q.eq("providerId", providerId))
865
+ .collect();
866
+ // Get all API calls (legacy table, currently empty)
867
+ const allCalls = await ctx.db
868
+ .query("apiCalls")
869
+ .withIndex("by_providerId", (q) => q.eq("providerId", providerId))
870
+ .collect();
871
+ // Find last completed payout
872
+ const completedPayouts = payouts
873
+ .filter((p) => p.status === "completed")
874
+ .sort((a, b) => b.periodEnd - a.periodEnd);
875
+ const lastPayoutEnd = completedPayouts[0]?.periodEnd || 0;
876
+ // Pending = all revenue since last payout
877
+ const pendingCalls = allCalls.filter((c) => c.timestamp > lastPayoutEnd);
878
+ const pendingAmount = pendingCalls.reduce((sum, c) => sum + c.costUsd, 0);
879
+ // Total earned all time
880
+ const totalEarned = allCalls.reduce((sum, c) => sum + c.costUsd, 0);
881
+ // Get provider for Stripe status
882
+ const provider = await ctx.db.get(providerId);
883
+ return {
884
+ pendingAmount,
885
+ totalEarned,
886
+ totalPaidOut: completedPayouts.reduce((sum, p) => sum + p.amountUsd, 0),
887
+ stripeConnected: !!provider?.stripeConnectId,
888
+ stripeOnboardingComplete: provider?.stripeOnboardingComplete || false,
889
+ payouts: payouts
890
+ .sort((a, b) => b.createdAt - a.createdAt)
891
+ .slice(0, 20)
892
+ .map((p) => ({
893
+ id: p._id,
894
+ amount: p.amountUsd,
895
+ status: p.status,
896
+ periodStart: p.periodStart,
897
+ periodEnd: p.periodEnd,
898
+ createdAt: p.createdAt,
899
+ completedAt: p.completedAt,
900
+ })),
901
+ };
902
+ },
903
+ });
904
+ // ============================================
905
+ // ADMIN QUERIES
906
+ // ============================================
907
+ // Get all providers (admin only)
908
+ export const getAllProviders = query({
909
+ handler: async (ctx) => {
910
+ return await ctx.db
911
+ .query("providers")
912
+ .order("desc")
913
+ .collect();
914
+ },
915
+ });
916
+ // Get all APIs (admin only)
917
+ export const getAllAPIs = query({
918
+ handler: async (ctx) => {
919
+ return await ctx.db
920
+ .query("providerAPIs")
921
+ .order("desc")
922
+ .collect();
923
+ },
924
+ });
925
+ // Helper function
926
+ function generateToken() {
927
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
928
+ let result = "";
929
+ for (let i = 0; i < 48; i++) {
930
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
931
+ }
932
+ return result;
933
+ }
934
+ // Debug: Update API name/description
935
+ export const debugUpdateAPI = mutation({
936
+ args: {
937
+ apiId: v.string(),
938
+ name: v.optional(v.string()),
939
+ description: v.optional(v.string()),
940
+ category: v.optional(v.string()),
941
+ status: v.optional(v.string()),
942
+ hasDirectCall: v.optional(v.boolean()),
943
+ },
944
+ handler: async (ctx, args) => {
945
+ const updates = {};
946
+ if (args.name)
947
+ updates.name = args.name;
948
+ if (args.description)
949
+ updates.description = args.description;
950
+ if (args.category)
951
+ updates.category = args.category;
952
+ if (args.status)
953
+ updates.status = args.status;
954
+ if (args.hasDirectCall !== undefined)
955
+ updates.hasDirectCall = args.hasDirectCall;
956
+ await ctx.db.patch(args.apiId, updates);
957
+ return { updated: true };
958
+ },
959
+ });
960
+ // ─── Workspace-native API management (no provider account needed) ─────────────
961
+ // Get all APIs listed by a workspace
962
+ export const getByWorkspaceId = query({
963
+ args: { workspaceId: v.id("workspaces") },
964
+ handler: async (ctx, { workspaceId }) => {
965
+ return await ctx.db
966
+ .query("providerAPIs")
967
+ .withIndex("by_workspaceId", (q) => q.eq("workspaceId", workspaceId))
968
+ .collect();
969
+ },
970
+ });
971
+ // List a new API directly from a workspace — no provider registration
972
+ export const createForWorkspace = mutation({
973
+ args: {
974
+ workspaceId: v.id("workspaces"),
975
+ name: v.string(),
976
+ description: v.string(),
977
+ category: v.string(),
978
+ openApiUrl: v.optional(v.string()),
979
+ docsUrl: v.optional(v.string()),
980
+ pricingModel: v.string(),
981
+ pricingNotes: v.optional(v.string()),
982
+ },
983
+ handler: async (ctx, args) => {
984
+ const id = await ctx.db.insert("providerAPIs", {
985
+ workspaceId: args.workspaceId,
986
+ name: args.name,
987
+ description: args.description,
988
+ category: args.category,
989
+ openApiUrl: args.openApiUrl,
990
+ docsUrl: args.docsUrl,
991
+ pricingModel: args.pricingModel,
992
+ pricingNotes: args.pricingNotes,
993
+ status: "active",
994
+ createdAt: Date.now(),
995
+ discoveryCount: 0,
996
+ });
997
+ return { id };
998
+ },
999
+ });
1000
+ // Delete an API owned by a workspace
1001
+ export const deleteForWorkspace = mutation({
1002
+ args: { apiId: v.id("providerAPIs"), workspaceId: v.id("workspaces") },
1003
+ handler: async (ctx, { apiId, workspaceId }) => {
1004
+ const api = await ctx.db.get(apiId);
1005
+ if (!api || api.workspaceId !== workspaceId) {
1006
+ throw new Error("Not found or unauthorized");
1007
+ }
1008
+ await ctx.db.delete(apiId);
1009
+ return { deleted: true };
1010
+ },
1011
+ });
1012
+ // Reset all discoveryCount to 0 (admin cleanup)
1013
+ export const resetDiscoveryCounts = mutation({
1014
+ args: {},
1015
+ handler: async (ctx) => {
1016
+ const apis = await ctx.db.query("providerAPIs").collect();
1017
+ let reset = 0;
1018
+ for (const api of apis) {
1019
+ if (api.discoveryCount > 0) {
1020
+ await ctx.db.patch(api._id, { discoveryCount: 0 });
1021
+ reset++;
1022
+ }
1023
+ }
1024
+ return { reset };
1025
+ },
1026
+ });
1027
+ //# sourceMappingURL=providers.js.map