@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.
- package/README.md +58 -30
- package/convex/adminActivate.d.ts +3 -0
- package/convex/adminActivate.d.ts.map +1 -1
- package/convex/adminActivate.js +46 -0
- package/convex/adminActivate.js.map +1 -1
- package/convex/adminActivate.ts +1 -2
- package/convex/adminStats.d.ts +9 -0
- package/convex/adminStats.d.ts.map +1 -1
- package/convex/adminStats.js +282 -0
- package/convex/adminStats.js.map +1 -1
- package/convex/adminStats.ts +5 -3
- package/convex/agents.d.ts +84 -0
- package/convex/agents.js +809 -0
- package/convex/analytics.d.ts +5 -0
- package/convex/analytics.js +167 -0
- package/convex/apiKeys.d.ts +6 -0
- package/convex/apiKeys.d.ts.map +1 -0
- package/convex/apiKeys.js +186 -0
- package/convex/apiKeys.js.map +1 -0
- package/convex/backfillAnalytics.d.ts +2 -0
- package/convex/backfillAnalytics.js +20 -0
- package/convex/backfillSearchLogs.d.ts +2 -0
- package/convex/backfillSearchLogs.js +29 -0
- package/convex/billing.d.ts +88 -0
- package/convex/billing.d.ts.map +1 -1
- package/convex/billing.js +643 -0
- package/convex/billing.js.map +1 -1
- package/convex/billing.ts +2 -14
- package/convex/capabilities.d.ts +9 -0
- package/convex/capabilities.js +145 -0
- package/convex/chains.d.ts +68 -0
- package/convex/chains.js +1105 -0
- package/convex/credits.d.ts +25 -0
- package/convex/credits.js +186 -0
- package/convex/crons.d.ts +3 -0
- package/convex/crons.js +17 -0
- package/convex/debugFilestackLogs.d.ts +2 -0
- package/convex/debugFilestackLogs.js +17 -0
- package/convex/debugGetToken.d.ts +2 -0
- package/convex/debugGetToken.js +18 -0
- package/convex/directCall.d.ts +72 -0
- package/convex/directCall.d.ts.map +1 -1
- package/convex/directCall.js +663 -0
- package/convex/directCall.js.map +1 -1
- package/convex/earnProgress.d.ts +58 -0
- package/convex/earnProgress.js +649 -0
- package/convex/email.d.ts +14 -0
- package/convex/email.js +300 -0
- package/convex/email.js.map +1 -1
- package/convex/feedback.d.ts +7 -0
- package/convex/feedback.js +227 -0
- package/convex/http.d.ts +3 -0
- package/convex/http.d.ts.map +1 -1
- package/convex/http.js +2135 -0
- package/convex/http.js.map +1 -1
- package/convex/http.ts +275 -3
- package/convex/inbound.d.ts +2 -0
- package/convex/inbound.js +32 -0
- package/convex/logs.d.ts +48 -0
- package/convex/logs.d.ts.map +1 -1
- package/convex/logs.js +623 -0
- package/convex/logs.js.map +1 -1
- package/convex/migrateFilestack.d.ts +2 -0
- package/convex/migrateFilestack.js +74 -0
- package/convex/migratePartnersProd.d.ts +8 -0
- package/convex/migratePartnersProd.js +165 -0
- package/convex/migratePratham.d.ts +2 -0
- package/convex/migratePratham.js +121 -0
- package/convex/migrateProviderWorkspaces.d.ts +13 -0
- package/convex/migrateProviderWorkspaces.d.ts.map +1 -1
- package/convex/migrateProviderWorkspaces.js +141 -0
- package/convex/migrateProviderWorkspaces.js.map +1 -1
- package/convex/mou.d.ts +6 -0
- package/convex/mou.js +82 -0
- package/convex/providerKeys.d.ts +31 -0
- package/convex/providerKeys.js +257 -0
- package/convex/providers.d.ts +35 -0
- package/convex/providers.d.ts.map +1 -1
- package/convex/providers.js +1027 -0
- package/convex/providers.js.map +1 -1
- package/convex/purchases.d.ts +7 -0
- package/convex/purchases.js +157 -0
- package/convex/ratelimit.d.ts +4 -0
- package/convex/ratelimit.js +91 -0
- package/convex/schema.ts +4 -4
- package/convex/searchLogs.d.ts +13 -0
- package/convex/searchLogs.d.ts.map +1 -1
- package/convex/searchLogs.js +241 -0
- package/convex/searchLogs.js.map +1 -1
- package/convex/seedAPILayerAPIs.d.ts +7 -0
- package/convex/seedAPILayerAPIs.js +177 -0
- package/convex/seedDirectCallConfigs.d.ts +2 -0
- package/convex/seedDirectCallConfigs.js +324 -0
- package/convex/seedPratham.d.ts +6 -0
- package/convex/seedPratham.d.ts.map +1 -1
- package/convex/seedPratham.js +149 -0
- package/convex/seedPratham.js.map +1 -1
- package/convex/seedPratham.ts +1 -2
- package/convex/spendAlerts.d.ts +36 -0
- package/convex/spendAlerts.js +380 -0
- package/convex/spendAlerts.js.map +1 -1
- package/convex/stripeActions.d.ts +19 -0
- package/convex/stripeActions.d.ts.map +1 -1
- package/convex/stripeActions.js +432 -0
- package/convex/stripeActions.js.map +1 -1
- package/convex/stripeActions.ts +25 -3
- package/convex/teams.d.ts +21 -0
- package/convex/teams.js +215 -0
- package/convex/telemetry.d.ts +4 -0
- package/convex/telemetry.js +74 -0
- package/convex/updateAPIStatus.d.ts +6 -0
- package/convex/updateAPIStatus.d.ts.map +1 -1
- package/convex/updateAPIStatus.js +39 -0
- package/convex/updateAPIStatus.js.map +1 -1
- package/convex/usage.d.ts +27 -0
- package/convex/usage.js +229 -0
- package/convex/waitlist.d.ts +4 -0
- package/convex/waitlist.js +49 -0
- package/convex/webhooks.d.ts +12 -0
- package/convex/webhooks.js +410 -0
- package/convex/workspaceSettings.d.ts +7 -0
- package/convex/workspaceSettings.d.ts.map +1 -0
- package/convex/workspaceSettings.js +128 -0
- package/convex/workspaceSettings.js.map +1 -0
- package/convex/workspaces.d.ts +33 -0
- package/convex/workspaces.d.ts.map +1 -1
- package/convex/workspaces.js +989 -0
- package/convex/workspaces.js.map +1 -1
- package/convex/workspaces.ts +18 -20
- package/dist/bin.js +0 -0
- package/dist/cli/commands/demo.js +1 -1
- package/dist/cli/commands/demo.js.map +1 -1
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/login.js.map +1 -1
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +15 -0
- package/dist/credentials.js.map +1 -1
- package/dist/discovery.js.map +1 -1
- package/dist/execute.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/open-apis.d.ts.map +1 -1
- package/dist/open-apis.js +94 -2
- package/dist/open-apis.js.map +1 -1
- package/dist/ui/errors.js.map +1 -1
- package/dist/ui/prompts.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/demo.ts +1 -1
- package/src/credentials.ts +16 -0
- package/src/index.ts +1 -1
- 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
|