@nordsym/apiclaw 1.8.6 → 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 +59 -31
- package/apiclaw-README.md +12 -12
- package/convex/_generated/api.d.ts +4 -0
- package/convex/adminActivate.d.ts.map +1 -1
- package/convex/adminActivate.js +1 -2
- package/convex/adminActivate.js.map +1 -1
- package/convex/adminActivate.ts +1 -2
- package/convex/adminStats.d.ts.map +1 -1
- package/convex/adminStats.js +5 -3
- package/convex/adminStats.js.map +1 -1
- package/convex/adminStats.ts +5 -3
- 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/apiKeys.ts +220 -0
- package/convex/billing.d.ts.map +1 -1
- package/convex/billing.js +1 -13
- package/convex/billing.js.map +1 -1
- package/convex/billing.ts +2 -14
- package/convex/directCall.d.ts.map +1 -1
- package/convex/directCall.js +49 -13
- package/convex/directCall.js.map +1 -1
- package/convex/directCall.ts +49 -14
- package/convex/email.js +5 -5
- package/convex/email.js.map +1 -1
- package/convex/email.ts +5 -5
- package/convex/http.d.ts.map +1 -1
- package/convex/http.js +756 -29
- package/convex/http.js.map +1 -1
- package/convex/http.ts +870 -40
- package/convex/logs.d.ts.map +1 -1
- package/convex/logs.js +21 -19
- package/convex/logs.js.map +1 -1
- package/convex/logs.ts +26 -22
- package/convex/migrateProviderWorkspaces.d.ts +11 -0
- package/convex/migrateProviderWorkspaces.d.ts.map +1 -1
- package/convex/migrateProviderWorkspaces.js +120 -34
- package/convex/migrateProviderWorkspaces.js.map +1 -1
- package/convex/migrateProviderWorkspaces.ts +154 -35
- package/convex/providers.d.ts.map +1 -1
- package/convex/providers.js +296 -191
- package/convex/providers.js.map +1 -1
- package/convex/providers.ts +313 -203
- package/convex/schema.ts +54 -5
- package/convex/searchLogs.d.ts.map +1 -1
- package/convex/searchLogs.js +2 -7
- package/convex/searchLogs.js.map +1 -1
- package/convex/searchLogs.ts +2 -6
- package/convex/seedPratham.d.ts.map +1 -1
- package/convex/seedPratham.js +2 -3
- package/convex/seedPratham.js.map +1 -1
- package/convex/seedPratham.ts +2 -3
- package/convex/spendAlerts.js +2 -2
- package/convex/spendAlerts.js.map +1 -1
- package/convex/spendAlerts.ts +2 -2
- package/convex/stripeActions.d.ts.map +1 -1
- package/convex/stripeActions.js +25 -4
- package/convex/stripeActions.js.map +1 -1
- package/convex/stripeActions.ts +26 -4
- package/convex/updateAPIStatus.d.ts.map +1 -1
- package/convex/updateAPIStatus.js +0 -1
- package/convex/updateAPIStatus.js.map +1 -1
- package/convex/updateAPIStatus.ts +2 -3
- 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/workspaceSettings.ts +136 -0
- package/convex/workspaces.d.ts.map +1 -1
- package/convex/workspaces.js +15 -17
- 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 +3 -3
- package/dist/cli/commands/demo.js.map +1 -1
- package/dist/cli/commands/doctor.js +1 -1
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/login.js +1 -1
- package/dist/cli/commands/login.js.map +1 -1
- package/dist/cli/commands/setup.js +2 -2
- 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 +1 -1
- package/dist/discovery.js.map +1 -1
- package/dist/execute.js +3 -3
- package/dist/execute.js.map +1 -1
- package/dist/index.js +14 -14
- 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 +16 -16
- package/dist/ui/errors.js.map +1 -1
- package/dist/ui/prompts.js +1 -1
- package/dist/ui/prompts.js.map +1 -1
- package/email-templates/filestack-provider-outreach.html +1 -1
- package/email-templates/partnership-template.html +1 -1
- package/email-templates/pratham-partnership-draft.html +2 -2
- package/package.json +2 -2
- package/reports/APIClaw-Session-Report-2026-04-05.pdf +0 -0
- package/reports/session-report-2026-04-05.html +433 -0
- package/src/cli/commands/demo.ts +3 -3
- package/src/cli/commands/doctor.ts +1 -1
- package/src/cli/commands/login.ts +1 -1
- package/src/cli/commands/setup.ts +2 -2
- package/src/credentials.ts +16 -0
- package/src/discovery.ts +1 -1
- package/src/execute.ts +3 -3
- package/src/index.ts +15 -15
- package/src/open-apis.ts +114 -4
- package/src/ui/errors.ts +16 -16
- package/src/ui/prompts.ts +1 -1
package/convex/providers.ts
CHANGED
|
@@ -62,14 +62,41 @@ export const registerProvider = mutation({
|
|
|
62
62
|
discoveryCount: 0,
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
-
//
|
|
66
|
-
const
|
|
67
|
-
|
|
65
|
+
// Find or create workspace for this provider email
|
|
66
|
+
const emailLower = args.provider.email.toLowerCase();
|
|
67
|
+
let workspace = await ctx.db
|
|
68
|
+
.query("workspaces")
|
|
69
|
+
.withIndex("by_email", (q) => q.eq("email", emailLower))
|
|
70
|
+
.first();
|
|
68
71
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
if (!workspace) {
|
|
73
|
+
const wsId = await ctx.db.insert("workspaces", {
|
|
74
|
+
email: emailLower,
|
|
75
|
+
status: "active",
|
|
76
|
+
tier: "free",
|
|
77
|
+
usageCount: 0,
|
|
78
|
+
usageLimit: 50,
|
|
79
|
+
weeklyUsageCount: 0,
|
|
80
|
+
weeklyUsageLimit: 50,
|
|
81
|
+
hourlyUsageCount: 0,
|
|
82
|
+
createdAt: now,
|
|
83
|
+
updatedAt: now,
|
|
84
|
+
});
|
|
85
|
+
workspace = await ctx.db.get(wsId);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Link provider → workspace (if not already)
|
|
89
|
+
const provider = await ctx.db.get(providerId);
|
|
90
|
+
if (provider && !(provider as any).workspaceId) {
|
|
91
|
+
await ctx.db.patch(providerId, { workspaceId: workspace!._id } as any);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Create unified session (agentSessions only - legacy sessions table deprecated)
|
|
95
|
+
const sessionToken = generateToken();
|
|
96
|
+
await ctx.db.insert("agentSessions", {
|
|
97
|
+
workspaceId: workspace!._id,
|
|
98
|
+
sessionToken,
|
|
99
|
+
lastUsedAt: now,
|
|
73
100
|
createdAt: now,
|
|
74
101
|
});
|
|
75
102
|
|
|
@@ -169,22 +196,21 @@ export const logDiscovery = mutation({
|
|
|
169
196
|
callerWorkspaceId: v.string(),
|
|
170
197
|
},
|
|
171
198
|
handler: async (ctx, args) => {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
199
|
+
// Resolve provider → workspace dynamically (no hardcoded email maps)
|
|
200
|
+
const providerNameLower = args.provider.toLowerCase();
|
|
201
|
+
const allProviders = await ctx.db.query("providers").collect();
|
|
202
|
+
const providerRecord = allProviders.find(
|
|
203
|
+
(p) => p.name.toLowerCase() === providerNameLower
|
|
204
|
+
);
|
|
205
|
+
if (!providerRecord || !(providerRecord as any).workspaceId) return { logged: false };
|
|
178
206
|
|
|
179
|
-
const workspace = await ctx.db
|
|
180
|
-
.query("workspaces")
|
|
181
|
-
.withIndex("by_email", (q) => q.eq("email", email))
|
|
182
|
-
.first();
|
|
207
|
+
const workspace = await ctx.db.get((providerRecord as any).workspaceId);
|
|
183
208
|
if (!workspace) return { logged: false };
|
|
209
|
+
const wsId = workspace._id as any;
|
|
184
210
|
|
|
185
211
|
// 1. Log to apiLogs (source of truth for Analytics)
|
|
186
212
|
await ctx.db.insert("apiLogs", {
|
|
187
|
-
workspaceId:
|
|
213
|
+
workspaceId: wsId,
|
|
188
214
|
sessionToken: "",
|
|
189
215
|
provider: args.provider,
|
|
190
216
|
action: `discovery:${args.query}`,
|
|
@@ -196,31 +222,25 @@ export const logDiscovery = mutation({
|
|
|
196
222
|
});
|
|
197
223
|
|
|
198
224
|
// 2. Increment discoveryCount on MATCHING APIs only
|
|
199
|
-
const
|
|
200
|
-
.query("
|
|
201
|
-
.withIndex("
|
|
202
|
-
.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
lastDiscoveredAt: Date.now(),
|
|
216
|
-
});
|
|
217
|
-
matched++;
|
|
218
|
-
}
|
|
225
|
+
const apis = await ctx.db
|
|
226
|
+
.query("providerAPIs")
|
|
227
|
+
.withIndex("by_providerId", (q) => q.eq("providerId", providerRecord._id))
|
|
228
|
+
.collect();
|
|
229
|
+
const queryLower = args.query.toLowerCase();
|
|
230
|
+
const queryWords = queryLower.split(/\s+/).filter((w: string) => w.length > 2);
|
|
231
|
+
|
|
232
|
+
let matched = 0;
|
|
233
|
+
for (const api of apis) {
|
|
234
|
+
const apiText = `${api.name} ${api.description || ""}`.toLowerCase();
|
|
235
|
+
if (queryWords.some((w: string) => apiText.includes(w))) {
|
|
236
|
+
await ctx.db.patch(api._id, {
|
|
237
|
+
discoveryCount: ((api as any).discoveryCount || 0) + 1,
|
|
238
|
+
lastDiscoveredAt: Date.now(),
|
|
239
|
+
});
|
|
240
|
+
matched++;
|
|
219
241
|
}
|
|
220
|
-
return { logged: true, matched };
|
|
221
242
|
}
|
|
222
|
-
|
|
223
|
-
return { logged: true, matched: 0 };
|
|
243
|
+
return { logged: true, matched };
|
|
224
244
|
},
|
|
225
245
|
});
|
|
226
246
|
|
|
@@ -228,24 +248,19 @@ export const logDiscovery = mutation({
|
|
|
228
248
|
export const trackDiscoveryByProvider = mutation({
|
|
229
249
|
args: { provider: v.string(), query: v.string() },
|
|
230
250
|
handler: async (ctx, args) => {
|
|
231
|
-
//
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (!email) return { updated: 0, error: "no email mapping" };
|
|
238
|
-
|
|
239
|
-
// Find provider record by email
|
|
240
|
-
const provider = await ctx.db
|
|
241
|
-
.query("providers")
|
|
242
|
-
.withIndex("by_email", (q) => q.eq("email", email))
|
|
243
|
-
.first();
|
|
251
|
+
// Resolve provider dynamically by name (no hardcoded email maps)
|
|
252
|
+
const providerNameLower = args.provider.toLowerCase();
|
|
253
|
+
const allProviders = await ctx.db.query("providers").collect();
|
|
254
|
+
const provider = allProviders.find(
|
|
255
|
+
(p) => p.name.toLowerCase() === providerNameLower
|
|
256
|
+
);
|
|
244
257
|
if (!provider) return { updated: 0, error: "provider not found" };
|
|
245
258
|
|
|
246
259
|
// Get all APIs for this provider
|
|
247
|
-
const
|
|
248
|
-
|
|
260
|
+
const providerApis = await ctx.db
|
|
261
|
+
.query("providerAPIs")
|
|
262
|
+
.withIndex("by_providerId", (q) => q.eq("providerId", provider._id))
|
|
263
|
+
.collect();
|
|
249
264
|
|
|
250
265
|
// Increment discoveryCount on ALL provider APIs
|
|
251
266
|
for (const api of providerApis) {
|
|
@@ -314,14 +329,14 @@ export const getProviderStats = query({
|
|
|
314
329
|
// DASHBOARD AUTH & SESSION FUNCTIONS
|
|
315
330
|
// ============================================
|
|
316
331
|
|
|
317
|
-
// Create magic link for email auth
|
|
332
|
+
// Create magic link for email auth (unified: writes to workspaceMagicLinks)
|
|
318
333
|
export const createMagicLink = mutation({
|
|
319
334
|
args: { email: v.string() },
|
|
320
335
|
handler: async (ctx, { email }) => {
|
|
321
336
|
const token = generateToken();
|
|
322
337
|
const expiresAt = Date.now() + 15 * 60 * 1000; // 15 minutes
|
|
323
338
|
|
|
324
|
-
await ctx.db.insert("
|
|
339
|
+
await ctx.db.insert("workspaceMagicLinks", {
|
|
325
340
|
email: email.toLowerCase(),
|
|
326
341
|
token,
|
|
327
342
|
expiresAt,
|
|
@@ -332,12 +347,12 @@ export const createMagicLink = mutation({
|
|
|
332
347
|
},
|
|
333
348
|
});
|
|
334
349
|
|
|
335
|
-
// Verify magic link and create session
|
|
350
|
+
// Verify magic link and create unified session (workspace + provider)
|
|
336
351
|
export const verifyMagicLink = mutation({
|
|
337
352
|
args: { token: v.string() },
|
|
338
353
|
handler: async (ctx, { token }) => {
|
|
339
354
|
const magicLink = await ctx.db
|
|
340
|
-
.query("
|
|
355
|
+
.query("workspaceMagicLinks")
|
|
341
356
|
.withIndex("by_token", (q) => q.eq("token", token))
|
|
342
357
|
.first();
|
|
343
358
|
|
|
@@ -356,6 +371,30 @@ export const verifyMagicLink = mutation({
|
|
|
356
371
|
// Mark as used
|
|
357
372
|
await ctx.db.patch(magicLink._id, { usedAt: Date.now() });
|
|
358
373
|
|
|
374
|
+
const now = Date.now();
|
|
375
|
+
|
|
376
|
+
// Find or create workspace
|
|
377
|
+
let workspace = await ctx.db
|
|
378
|
+
.query("workspaces")
|
|
379
|
+
.withIndex("by_email", (q) => q.eq("email", magicLink.email))
|
|
380
|
+
.first();
|
|
381
|
+
|
|
382
|
+
if (!workspace) {
|
|
383
|
+
const wsId = await ctx.db.insert("workspaces", {
|
|
384
|
+
email: magicLink.email,
|
|
385
|
+
status: "active",
|
|
386
|
+
tier: "free",
|
|
387
|
+
usageCount: 0,
|
|
388
|
+
usageLimit: 50,
|
|
389
|
+
weeklyUsageCount: 0,
|
|
390
|
+
weeklyUsageLimit: 50,
|
|
391
|
+
hourlyUsageCount: 0,
|
|
392
|
+
createdAt: now,
|
|
393
|
+
updatedAt: now,
|
|
394
|
+
});
|
|
395
|
+
workspace = await ctx.db.get(wsId);
|
|
396
|
+
}
|
|
397
|
+
|
|
359
398
|
// Find or create provider
|
|
360
399
|
let provider = await ctx.db
|
|
361
400
|
.query("providers")
|
|
@@ -367,21 +406,23 @@ export const verifyMagicLink = mutation({
|
|
|
367
406
|
email: magicLink.email,
|
|
368
407
|
name: magicLink.email.split("@")[0],
|
|
369
408
|
status: "approved",
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
409
|
+
workspaceId: workspace!._id,
|
|
410
|
+
createdAt: now,
|
|
411
|
+
updatedAt: now,
|
|
412
|
+
} as any);
|
|
373
413
|
provider = await ctx.db.get(providerId);
|
|
414
|
+
} else if (!(provider as any).workspaceId) {
|
|
415
|
+
// Link existing provider to workspace
|
|
416
|
+
await ctx.db.patch(provider._id, { workspaceId: workspace!._id } as any);
|
|
374
417
|
}
|
|
375
418
|
|
|
376
|
-
// Create session
|
|
419
|
+
// Create unified session (agentSessions)
|
|
377
420
|
const sessionToken = generateToken();
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
expiresAt: sessionExpiresAt,
|
|
384
|
-
createdAt: Date.now(),
|
|
421
|
+
await ctx.db.insert("agentSessions", {
|
|
422
|
+
workspaceId: workspace!._id,
|
|
423
|
+
sessionToken,
|
|
424
|
+
lastUsedAt: now,
|
|
425
|
+
createdAt: now,
|
|
385
426
|
});
|
|
386
427
|
|
|
387
428
|
return {
|
|
@@ -396,10 +437,34 @@ export const verifyMagicLink = mutation({
|
|
|
396
437
|
},
|
|
397
438
|
});
|
|
398
439
|
|
|
399
|
-
// Get current session
|
|
440
|
+
// Get current session (unified: tries agentSessions first, falls back to legacy sessions)
|
|
400
441
|
export const getSession = query({
|
|
401
442
|
args: { token: v.string() },
|
|
402
443
|
handler: async (ctx, { token }) => {
|
|
444
|
+
// 1. Try unified agentSessions (by sessionToken)
|
|
445
|
+
const agentSession = await ctx.db
|
|
446
|
+
.query("agentSessions")
|
|
447
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
|
|
448
|
+
.first();
|
|
449
|
+
|
|
450
|
+
if (agentSession) {
|
|
451
|
+
// Resolve provider via workspace
|
|
452
|
+
const provider = await ctx.db
|
|
453
|
+
.query("providers")
|
|
454
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", agentSession.workspaceId))
|
|
455
|
+
.first();
|
|
456
|
+
|
|
457
|
+
if (!provider) return null;
|
|
458
|
+
|
|
459
|
+
return {
|
|
460
|
+
providerId: provider._id,
|
|
461
|
+
email: provider.email,
|
|
462
|
+
name: provider.name,
|
|
463
|
+
stripeOnboardingComplete: (provider as any).stripeOnboardingComplete,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// 2. Fallback: legacy sessions table (for tokens created before migration)
|
|
403
468
|
const session = await ctx.db
|
|
404
469
|
.query("sessions")
|
|
405
470
|
.withIndex("by_token", (q) => q.eq("token", token))
|
|
@@ -499,19 +564,35 @@ export const addAPI = mutation({
|
|
|
499
564
|
}),
|
|
500
565
|
},
|
|
501
566
|
handler: async (ctx, args) => {
|
|
502
|
-
//
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
567
|
+
// Unified session lookup
|
|
568
|
+
let providerId: any = null;
|
|
569
|
+
|
|
570
|
+
const agentSession = await ctx.db
|
|
571
|
+
.query("agentSessions")
|
|
572
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.token))
|
|
506
573
|
.first();
|
|
507
574
|
|
|
508
|
-
if (
|
|
509
|
-
|
|
575
|
+
if (agentSession) {
|
|
576
|
+
const prov = await ctx.db
|
|
577
|
+
.query("providers")
|
|
578
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", agentSession.workspaceId))
|
|
579
|
+
.first();
|
|
580
|
+
if (prov) providerId = prov._id;
|
|
581
|
+
} else {
|
|
582
|
+
const session = await ctx.db
|
|
583
|
+
.query("sessions")
|
|
584
|
+
.withIndex("by_token", (q) => q.eq("token", args.token))
|
|
585
|
+
.first();
|
|
586
|
+
if (session && session.expiresAt >= Date.now()) {
|
|
587
|
+
providerId = session.providerId;
|
|
588
|
+
}
|
|
510
589
|
}
|
|
511
590
|
|
|
591
|
+
if (!providerId) throw new Error("Invalid or expired session");
|
|
592
|
+
|
|
512
593
|
const now = Date.now();
|
|
513
594
|
const apiId = await ctx.db.insert("providerAPIs", {
|
|
514
|
-
providerId
|
|
595
|
+
providerId,
|
|
515
596
|
name: args.api.name,
|
|
516
597
|
description: args.api.description,
|
|
517
598
|
category: args.api.category,
|
|
@@ -536,19 +617,35 @@ export const deleteAPI = mutation({
|
|
|
536
617
|
apiId: v.string(),
|
|
537
618
|
},
|
|
538
619
|
handler: async (ctx, args) => {
|
|
539
|
-
//
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
620
|
+
// Unified session lookup
|
|
621
|
+
let providerId: any = null;
|
|
622
|
+
|
|
623
|
+
const agentSession = await ctx.db
|
|
624
|
+
.query("agentSessions")
|
|
625
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.token))
|
|
543
626
|
.first();
|
|
544
627
|
|
|
545
|
-
if (
|
|
546
|
-
|
|
628
|
+
if (agentSession) {
|
|
629
|
+
const prov = await ctx.db
|
|
630
|
+
.query("providers")
|
|
631
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", agentSession.workspaceId))
|
|
632
|
+
.first();
|
|
633
|
+
if (prov) providerId = prov._id;
|
|
634
|
+
} else {
|
|
635
|
+
const session = await ctx.db
|
|
636
|
+
.query("sessions")
|
|
637
|
+
.withIndex("by_token", (q) => q.eq("token", args.token))
|
|
638
|
+
.first();
|
|
639
|
+
if (session && session.expiresAt >= Date.now()) {
|
|
640
|
+
providerId = session.providerId;
|
|
641
|
+
}
|
|
547
642
|
}
|
|
548
643
|
|
|
644
|
+
if (!providerId) throw new Error("Invalid or expired session");
|
|
645
|
+
|
|
549
646
|
// Get the API and verify ownership
|
|
550
647
|
const api = await ctx.db.get(args.apiId as any);
|
|
551
|
-
if (!api || (api as any).providerId !==
|
|
648
|
+
if (!api || (api as any).providerId !== providerId) {
|
|
552
649
|
throw new Error("API not found or unauthorized");
|
|
553
650
|
}
|
|
554
651
|
|
|
@@ -659,19 +756,51 @@ export const debugListProviders = query({
|
|
|
659
756
|
|
|
660
757
|
export const getAnalytics = query({
|
|
661
758
|
args: {
|
|
662
|
-
token: v.string(),
|
|
759
|
+
token: v.optional(v.string()),
|
|
760
|
+
workspaceId: v.optional(v.string()), // Direct workspace ID (used by /workspace page)
|
|
663
761
|
period: v.optional(v.string()), // "week", "month", "all"
|
|
664
762
|
},
|
|
665
|
-
handler: async (ctx, { token, period = "month" }) => {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
763
|
+
handler: async (ctx, { token, workspaceId: wsIdArg, period = "month" }) => {
|
|
764
|
+
let providerId: any = null;
|
|
765
|
+
|
|
766
|
+
// Path 1: Direct workspaceId (from workspace page)
|
|
767
|
+
if (wsIdArg) {
|
|
768
|
+
const prov = await ctx.db
|
|
769
|
+
.query("providers")
|
|
770
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", wsIdArg as any))
|
|
771
|
+
.first();
|
|
772
|
+
if (prov) providerId = prov._id;
|
|
773
|
+
}
|
|
670
774
|
|
|
671
|
-
|
|
672
|
-
|
|
775
|
+
// Path 2: Session token lookup (from provider dashboard / API)
|
|
776
|
+
if (!providerId && token) {
|
|
777
|
+
const agentSession = await ctx.db
|
|
778
|
+
.query("agentSessions")
|
|
779
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
|
|
780
|
+
.first();
|
|
781
|
+
|
|
782
|
+
if (agentSession) {
|
|
783
|
+
const prov = await ctx.db
|
|
784
|
+
.query("providers")
|
|
785
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", agentSession.workspaceId))
|
|
786
|
+
.first();
|
|
787
|
+
if (prov) providerId = prov._id;
|
|
788
|
+
} else {
|
|
789
|
+
const session = await ctx.db
|
|
790
|
+
.query("sessions")
|
|
791
|
+
.withIndex("by_token", (q) => q.eq("token", token))
|
|
792
|
+
.first();
|
|
793
|
+
if (session && session.expiresAt >= Date.now()) {
|
|
794
|
+
providerId = session.providerId;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
673
797
|
}
|
|
674
798
|
|
|
799
|
+
if (!providerId) return null;
|
|
800
|
+
|
|
801
|
+
const provider = await ctx.db.get(providerId) as any;
|
|
802
|
+
if (!provider) return null;
|
|
803
|
+
|
|
675
804
|
const now = Date.now();
|
|
676
805
|
const periodMs = {
|
|
677
806
|
week: 7 * 24 * 60 * 60 * 1000,
|
|
@@ -681,139 +810,101 @@ export const getAnalytics = query({
|
|
|
681
810
|
|
|
682
811
|
const startTime = now - periodMs;
|
|
683
812
|
|
|
684
|
-
//
|
|
685
|
-
const
|
|
686
|
-
|
|
687
|
-
|
|
813
|
+
// Provider name key used in apiLogs.provider (lowercase)
|
|
814
|
+
const providerKey = (provider.name as string).toLowerCase();
|
|
815
|
+
|
|
816
|
+
// Get real data from apiLogs (source of truth for all API activity)
|
|
817
|
+
const allLogs = await ctx.db
|
|
818
|
+
.query("apiLogs")
|
|
819
|
+
.withIndex("by_provider", (q) => q.eq("provider", providerKey))
|
|
688
820
|
.collect();
|
|
689
821
|
|
|
690
|
-
const
|
|
822
|
+
const periodLogs = allLogs.filter((l) => l.createdAt >= startTime);
|
|
823
|
+
|
|
824
|
+
// Split into direct calls vs discovery
|
|
825
|
+
const directCalls = periodLogs.filter((l) => !(l as any).action?.startsWith("discovery:"));
|
|
826
|
+
const discoveries = periodLogs.filter((l) => (l as any).action?.startsWith("discovery:"));
|
|
691
827
|
|
|
692
828
|
// Calculate metrics
|
|
693
|
-
const totalCalls =
|
|
694
|
-
const
|
|
695
|
-
const
|
|
696
|
-
const successCount =
|
|
829
|
+
const totalCalls = directCalls.length;
|
|
830
|
+
const totalDiscoveries = discoveries.length;
|
|
831
|
+
const uniqueCallers = new Set(periodLogs.map((l) => (l as any).callerWorkspaceId || l.workspaceId)).size;
|
|
832
|
+
const successCount = directCalls.filter((l) => l.status === "success").length;
|
|
697
833
|
const successRate = totalCalls > 0 ? (successCount / totalCalls) * 100 : 100;
|
|
698
|
-
const avgLatency = totalCalls > 0
|
|
699
|
-
?
|
|
834
|
+
const avgLatency = totalCalls > 0
|
|
835
|
+
? Math.round(directCalls.reduce((sum, l) => sum + l.latencyMs, 0) / totalCalls)
|
|
700
836
|
: 0;
|
|
701
837
|
|
|
702
838
|
// Calls over time (daily buckets)
|
|
703
|
-
const callsByDay: Record<string, { calls: number;
|
|
839
|
+
const callsByDay: Record<string, { calls: number; discoveries: number; success: number }> = {};
|
|
704
840
|
|
|
705
|
-
|
|
706
|
-
const day = new Date(
|
|
841
|
+
periodLogs.forEach((log) => {
|
|
842
|
+
const day = new Date(log.createdAt).toISOString().split("T")[0];
|
|
707
843
|
if (!callsByDay[day]) {
|
|
708
|
-
callsByDay[day] = { calls: 0,
|
|
844
|
+
callsByDay[day] = { calls: 0, discoveries: 0, success: 0 };
|
|
845
|
+
}
|
|
846
|
+
if ((log as any).action?.startsWith("discovery:")) {
|
|
847
|
+
callsByDay[day].discoveries += 1;
|
|
848
|
+
} else {
|
|
849
|
+
callsByDay[day].calls += 1;
|
|
850
|
+
if (log.status === "success") callsByDay[day].success += 1;
|
|
709
851
|
}
|
|
710
|
-
callsByDay[day].calls += 1;
|
|
711
|
-
callsByDay[day].revenue += call.creditsUsed / 100;
|
|
712
|
-
if (call.success) callsByDay[day].success += 1;
|
|
713
852
|
});
|
|
714
853
|
|
|
715
|
-
// Top agents (users)
|
|
716
|
-
const agentCallCounts: Record<string, number> = {};
|
|
717
|
-
periodCalls.forEach((call) => {
|
|
718
|
-
agentCallCounts[call.userId] = (agentCallCounts[call.userId] || 0) + 1;
|
|
719
|
-
});
|
|
720
|
-
const topAgents = Object.entries(agentCallCounts)
|
|
721
|
-
.sort((a, b) => b[1] - a[1])
|
|
722
|
-
.slice(0, 10)
|
|
723
|
-
.map(([agentId, calls]) => ({ agentId, calls }));
|
|
724
|
-
|
|
725
854
|
// Top actions
|
|
726
855
|
const actionCallCounts: Record<string, number> = {};
|
|
727
|
-
|
|
728
|
-
|
|
856
|
+
directCalls.forEach((log) => {
|
|
857
|
+
const action = (log as any).action || "unknown";
|
|
858
|
+
actionCallCounts[action] = (actionCallCounts[action] || 0) + 1;
|
|
729
859
|
});
|
|
730
860
|
const topActions = Object.entries(actionCallCounts)
|
|
731
861
|
.sort((a, b) => b[1] - a[1])
|
|
732
862
|
.slice(0, 10)
|
|
733
863
|
.map(([actionName, calls]) => ({ actionName, calls }));
|
|
734
864
|
|
|
865
|
+
// Top callers (workspace IDs that called this provider)
|
|
866
|
+
const callerCounts: Record<string, number> = {};
|
|
867
|
+
directCalls.forEach((log) => {
|
|
868
|
+
const caller = (log as any).callerWorkspaceId || "anonymous";
|
|
869
|
+
callerCounts[caller] = (callerCounts[caller] || 0) + 1;
|
|
870
|
+
});
|
|
871
|
+
const topAgents = Object.entries(callerCounts)
|
|
872
|
+
.sort((a, b) => b[1] - a[1])
|
|
873
|
+
.slice(0, 10)
|
|
874
|
+
.map(([agentId, calls]) => ({ agentId, calls }));
|
|
875
|
+
|
|
735
876
|
// Get provider's APIs
|
|
736
877
|
const apis = await ctx.db
|
|
737
878
|
.query("providerAPIs")
|
|
738
|
-
.withIndex("by_providerId", (q) => q.eq("providerId",
|
|
739
|
-
.collect();
|
|
740
|
-
|
|
741
|
-
// Get Direct Call configs to map directCallId to apiId
|
|
742
|
-
const directCallConfigs = await ctx.db
|
|
743
|
-
.query("providerDirectCall")
|
|
744
|
-
.withIndex("by_providerId", (q) => q.eq("providerId", session.providerId))
|
|
879
|
+
.withIndex("by_providerId", (q) => q.eq("providerId", providerId))
|
|
745
880
|
.collect();
|
|
746
881
|
|
|
747
|
-
//
|
|
748
|
-
const
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
callsByApiId[dc.apiId as string] = callsByDirectCallId[dc._id as string] || 0;
|
|
759
|
-
}
|
|
760
|
-
});
|
|
761
|
-
|
|
762
|
-
// Preview data for providers with no usage yet
|
|
763
|
-
const isPreview = totalCalls === 0;
|
|
764
|
-
|
|
765
|
-
if (isPreview) {
|
|
766
|
-
// Generate preview data so providers can see what the dashboard looks like
|
|
767
|
-
const previewDays = [];
|
|
768
|
-
for (let i = 13; i >= 0; i--) {
|
|
769
|
-
const date = new Date(now - i * 24 * 60 * 60 * 1000).toISOString().split("T")[0];
|
|
770
|
-
previewDays.push({
|
|
771
|
-
date,
|
|
772
|
-
calls: Math.floor(Math.random() * 50) + 10,
|
|
773
|
-
revenue: Math.random() * 5,
|
|
774
|
-
});
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
return {
|
|
778
|
-
totalCalls: 847,
|
|
779
|
-
uniqueAgents: 23,
|
|
780
|
-
totalRevenue: 42.35,
|
|
781
|
-
successRate: 98.2,
|
|
782
|
-
avgLatency: 145,
|
|
783
|
-
callsByDay: previewDays,
|
|
784
|
-
topAgents: [
|
|
785
|
-
{ agentId: "agent_demo_1", calls: 234 },
|
|
786
|
-
{ agentId: "agent_demo_2", calls: 189 },
|
|
787
|
-
{ agentId: "agent_demo_3", calls: 156 },
|
|
788
|
-
{ agentId: "agent_demo_4", calls: 98 },
|
|
789
|
-
{ agentId: "agent_demo_5", calls: 67 },
|
|
790
|
-
],
|
|
791
|
-
topActions: [
|
|
792
|
-
{ actionName: "send_message", calls: 412 },
|
|
793
|
-
{ actionName: "get_status", calls: 289 },
|
|
794
|
-
{ actionName: "create_invoice", calls: 146 },
|
|
795
|
-
],
|
|
796
|
-
apis: apis.map((api) => ({
|
|
797
|
-
id: api._id,
|
|
798
|
-
name: api.name,
|
|
799
|
-
calls: Math.floor(Math.random() * 200) + 50,
|
|
800
|
-
status: api.status,
|
|
801
|
-
})),
|
|
802
|
-
isPreview: true,
|
|
803
|
-
};
|
|
882
|
+
// Per-API call counts (match action name to API name)
|
|
883
|
+
const apiCallCounts: Record<string, number> = {};
|
|
884
|
+
const apiDiscoveryCounts: Record<string, number> = {};
|
|
885
|
+
for (const api of apis) {
|
|
886
|
+
const apiNameLower = api.name.toLowerCase();
|
|
887
|
+
apiCallCounts[api._id as string] = directCalls.filter(
|
|
888
|
+
(l) => (l as any).action?.toLowerCase().includes(apiNameLower)
|
|
889
|
+
).length;
|
|
890
|
+
apiDiscoveryCounts[api._id as string] = discoveries.filter(
|
|
891
|
+
(l) => (l as any).action?.toLowerCase().includes(apiNameLower)
|
|
892
|
+
).length;
|
|
804
893
|
}
|
|
805
894
|
|
|
806
895
|
return {
|
|
807
896
|
totalCalls,
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
897
|
+
totalDiscoveries,
|
|
898
|
+
uniqueAgents: uniqueCallers,
|
|
899
|
+
totalRevenue: 0, // Revenue tracking not yet implemented
|
|
900
|
+
successRate: Math.round(successRate * 10) / 10,
|
|
811
901
|
avgLatency,
|
|
812
902
|
callsByDay: Object.entries(callsByDay)
|
|
813
903
|
.map(([date, data]) => ({
|
|
814
904
|
date,
|
|
815
905
|
calls: data.calls,
|
|
816
|
-
|
|
906
|
+
discoveries: data.discoveries,
|
|
907
|
+
revenue: 0,
|
|
817
908
|
}))
|
|
818
909
|
.sort((a, b) => a.date.localeCompare(b.date)),
|
|
819
910
|
topAgents,
|
|
@@ -821,7 +912,8 @@ export const getAnalytics = query({
|
|
|
821
912
|
apis: apis.map((api) => ({
|
|
822
913
|
id: api._id,
|
|
823
914
|
name: api.name,
|
|
824
|
-
calls:
|
|
915
|
+
calls: apiCallCounts[api._id as string] || 0,
|
|
916
|
+
discoveries: apiDiscoveryCounts[api._id as string] || 0,
|
|
825
917
|
status: api.status,
|
|
826
918
|
})),
|
|
827
919
|
isPreview: false,
|
|
@@ -833,28 +925,46 @@ export const getAnalytics = query({
|
|
|
833
925
|
// DASHBOARD EARNINGS
|
|
834
926
|
// ============================================
|
|
835
927
|
|
|
928
|
+
// Earnings placeholder - partners (APILayer, Filestack) don't earn per-call revenue yet
|
|
836
929
|
export const getEarnings = query({
|
|
837
930
|
args: { token: v.string() },
|
|
838
931
|
handler: async (ctx, { token }) => {
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
932
|
+
// Unified session lookup
|
|
933
|
+
let providerId: any = null;
|
|
934
|
+
|
|
935
|
+
const agentSession = await ctx.db
|
|
936
|
+
.query("agentSessions")
|
|
937
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
|
|
842
938
|
.first();
|
|
843
939
|
|
|
844
|
-
if (
|
|
845
|
-
|
|
940
|
+
if (agentSession) {
|
|
941
|
+
const prov = await ctx.db
|
|
942
|
+
.query("providers")
|
|
943
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", agentSession.workspaceId))
|
|
944
|
+
.first();
|
|
945
|
+
if (prov) providerId = prov._id;
|
|
946
|
+
} else {
|
|
947
|
+
const session = await ctx.db
|
|
948
|
+
.query("sessions")
|
|
949
|
+
.withIndex("by_token", (q) => q.eq("token", token))
|
|
950
|
+
.first();
|
|
951
|
+
if (session && session.expiresAt >= Date.now()) {
|
|
952
|
+
providerId = session.providerId;
|
|
953
|
+
}
|
|
846
954
|
}
|
|
847
955
|
|
|
848
|
-
|
|
956
|
+
if (!providerId) return null;
|
|
957
|
+
|
|
958
|
+
// Get all payouts (currently empty for all providers)
|
|
849
959
|
const payouts = await ctx.db
|
|
850
960
|
.query("payouts")
|
|
851
|
-
.withIndex("by_providerId", (q) => q.eq("providerId",
|
|
961
|
+
.withIndex("by_providerId", (q) => q.eq("providerId", providerId))
|
|
852
962
|
.collect();
|
|
853
963
|
|
|
854
|
-
// Get all API calls
|
|
964
|
+
// Get all API calls (legacy table, currently empty)
|
|
855
965
|
const allCalls = await ctx.db
|
|
856
966
|
.query("apiCalls")
|
|
857
|
-
.withIndex("by_providerId", (q) => q.eq("providerId",
|
|
967
|
+
.withIndex("by_providerId", (q) => q.eq("providerId", providerId))
|
|
858
968
|
.collect();
|
|
859
969
|
|
|
860
970
|
// Find last completed payout
|
|
@@ -872,7 +982,7 @@ export const getEarnings = query({
|
|
|
872
982
|
const totalEarned = allCalls.reduce((sum, c) => sum + c.costUsd, 0);
|
|
873
983
|
|
|
874
984
|
// Get provider for Stripe status
|
|
875
|
-
const provider = await ctx.db.get(
|
|
985
|
+
const provider = await ctx.db.get(providerId);
|
|
876
986
|
|
|
877
987
|
return {
|
|
878
988
|
pendingAmount,
|