@nordsym/apiclaw 2.1.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/README.md +15 -2
  2. package/dist/bin-http.js +0 -0
  3. package/dist/bin.bundled.js +79288 -0
  4. package/dist/funnel-client.d.ts +24 -0
  5. package/dist/funnel-client.d.ts.map +1 -0
  6. package/dist/funnel-client.js +131 -0
  7. package/dist/funnel-client.js.map +1 -0
  8. package/dist/funnel.test.d.ts +2 -0
  9. package/dist/funnel.test.d.ts.map +1 -0
  10. package/dist/funnel.test.js +145 -0
  11. package/dist/funnel.test.js.map +1 -0
  12. package/dist/gateway-client.d.ts.map +1 -1
  13. package/dist/gateway-client.js +24 -2
  14. package/dist/gateway-client.js.map +1 -1
  15. package/dist/index.bundled.js +61263 -0
  16. package/dist/index.js +161 -74
  17. package/dist/index.js.map +1 -1
  18. package/dist/postinstall.d.ts +0 -5
  19. package/dist/postinstall.d.ts.map +1 -1
  20. package/dist/postinstall.js +24 -3
  21. package/dist/postinstall.js.map +1 -1
  22. package/dist/registration-guard.d.ts +29 -0
  23. package/dist/registration-guard.d.ts.map +1 -0
  24. package/dist/registration-guard.js +87 -0
  25. package/dist/registration-guard.js.map +1 -0
  26. package/package.json +7 -2
  27. package/.claude/settings.local.json +0 -9
  28. package/.env.prod +0 -1
  29. package/apiclaw-README.md +0 -494
  30. package/convex/_generated/api.d.ts +0 -137
  31. package/convex/_generated/api.js +0 -23
  32. package/convex/_generated/dataModel.d.ts +0 -60
  33. package/convex/_generated/server.d.ts +0 -143
  34. package/convex/_generated/server.js +0 -93
  35. package/convex/adminActivate.ts +0 -53
  36. package/convex/adminStats.ts +0 -306
  37. package/convex/agents.ts +0 -939
  38. package/convex/analytics.ts +0 -187
  39. package/convex/apiKeys.ts +0 -220
  40. package/convex/backfillAnalytics.ts +0 -272
  41. package/convex/backfillSearchLogs.ts +0 -35
  42. package/convex/billing.ts +0 -834
  43. package/convex/capabilities.ts +0 -157
  44. package/convex/chains.ts +0 -1318
  45. package/convex/credits.ts +0 -211
  46. package/convex/crons.ts +0 -50
  47. package/convex/debugFilestackLogs.ts +0 -16
  48. package/convex/debugGetToken.ts +0 -18
  49. package/convex/directCall.ts +0 -713
  50. package/convex/earnProgress.ts +0 -753
  51. package/convex/email.ts +0 -329
  52. package/convex/feedback.ts +0 -265
  53. package/convex/http.ts +0 -3430
  54. package/convex/inbound.ts +0 -32
  55. package/convex/logs.ts +0 -701
  56. package/convex/migrateFilestack.ts +0 -81
  57. package/convex/migratePartnersProd.ts +0 -174
  58. package/convex/migratePratham.ts +0 -126
  59. package/convex/migrateProviderWorkspaces.ts +0 -175
  60. package/convex/mou.ts +0 -91
  61. package/convex/providerKeys.ts +0 -289
  62. package/convex/providers.ts +0 -1135
  63. package/convex/purchases.ts +0 -183
  64. package/convex/ratelimit.ts +0 -104
  65. package/convex/schema.ts +0 -869
  66. package/convex/searchLogs.ts +0 -265
  67. package/convex/seedAPILayerAPIs.ts +0 -191
  68. package/convex/seedDirectCallConfigs.ts +0 -336
  69. package/convex/seedPratham.ts +0 -149
  70. package/convex/spendAlerts.ts +0 -442
  71. package/convex/stripeActions.ts +0 -607
  72. package/convex/teams.ts +0 -243
  73. package/convex/telemetry.ts +0 -81
  74. package/convex/tsconfig.json +0 -25
  75. package/convex/updateAPIStatus.ts +0 -44
  76. package/convex/usage.ts +0 -260
  77. package/convex/usageReports.ts +0 -357
  78. package/convex/waitlist.ts +0 -55
  79. package/convex/webhooks.ts +0 -494
  80. package/convex/workspaceSettings.ts +0 -143
  81. package/convex/workspaces.ts +0 -1331
  82. package/convex.json +0 -3
  83. package/direct-test.mjs +0 -51
  84. package/email-templates/filestack-provider-outreach.html +0 -162
  85. package/email-templates/partnership-template.html +0 -116
  86. package/email-templates/pratham-draft-preview.txt +0 -57
  87. package/email-templates/pratham-partnership-draft.html +0 -141
  88. package/reports/APIClaw-Session-Report-2026-04-05.pdf +0 -0
  89. package/reports/pipeline/PIPELINE-REPORT.json +0 -153
  90. package/reports/pipeline/acquire_apisguru.json +0 -17
  91. package/reports/pipeline/capabilities.json +0 -38
  92. package/reports/pipeline/discover_azure_recursive.json +0 -1551
  93. package/reports/pipeline/discover_github.json +0 -25
  94. package/reports/pipeline/discover_github_repos.json +0 -49
  95. package/reports/pipeline/discover_swaggerhub.json +0 -24
  96. package/reports/pipeline/discover_well_known.json +0 -23
  97. package/reports/pipeline/fetch_specs.json +0 -19
  98. package/reports/pipeline/generate_providers.json +0 -14
  99. package/reports/pipeline/match_registry.json +0 -11
  100. package/reports/pipeline/parse_specs.json +0 -17
  101. package/reports/pipeline/promote_candidates.json +0 -34
  102. package/reports/pipeline/validate.json +0 -30
  103. package/reports/pipeline/validate_smoke_details.json +0 -3835
  104. package/reports/session-report-2026-04-05.html +0 -433
  105. package/seed-apis-direct.mjs +0 -106
  106. package/src/access-control.ts +0 -174
  107. package/src/adapters/base.ts +0 -364
  108. package/src/adapters/claude-desktop.ts +0 -41
  109. package/src/adapters/cline.ts +0 -88
  110. package/src/adapters/continue.ts +0 -91
  111. package/src/adapters/cursor.ts +0 -43
  112. package/src/adapters/custom.ts +0 -188
  113. package/src/adapters/detect.ts +0 -202
  114. package/src/adapters/index.ts +0 -47
  115. package/src/adapters/windsurf.ts +0 -44
  116. package/src/bin-http.ts +0 -45
  117. package/src/bin.ts +0 -34
  118. package/src/capability-router.ts +0 -331
  119. package/src/chainExecutor.ts +0 -730
  120. package/src/chainResolver.test.ts +0 -246
  121. package/src/chainResolver.ts +0 -658
  122. package/src/cli/commands/demo.ts +0 -109
  123. package/src/cli/commands/doctor.ts +0 -435
  124. package/src/cli/commands/index.ts +0 -9
  125. package/src/cli/commands/login.ts +0 -203
  126. package/src/cli/commands/mcp-install.ts +0 -373
  127. package/src/cli/commands/restore.ts +0 -333
  128. package/src/cli/commands/setup.ts +0 -297
  129. package/src/cli/commands/uninstall.ts +0 -240
  130. package/src/cli/index.ts +0 -148
  131. package/src/cli.ts +0 -370
  132. package/src/confirmation.ts +0 -296
  133. package/src/credentials.ts +0 -455
  134. package/src/credits.ts +0 -329
  135. package/src/crypto.ts +0 -75
  136. package/src/discovery.ts +0 -568
  137. package/src/enterprise/env.ts +0 -156
  138. package/src/enterprise/index.ts +0 -7
  139. package/src/enterprise/script-generator.ts +0 -481
  140. package/src/execute-dynamic.ts +0 -617
  141. package/src/execute.ts +0 -2386
  142. package/src/gateway-client.ts +0 -192
  143. package/src/hivr-whitelist.ts +0 -110
  144. package/src/http-api.ts +0 -286
  145. package/src/http-server-minimal.ts +0 -154
  146. package/src/index.ts +0 -2611
  147. package/src/intelligent-gateway.ts +0 -339
  148. package/src/mcp-analytics.ts +0 -156
  149. package/src/metered.ts +0 -149
  150. package/src/open-apis-generated.ts +0 -157
  151. package/src/open-apis.ts +0 -558
  152. package/src/postinstall.ts +0 -18
  153. package/src/product-whitelist.ts +0 -246
  154. package/src/proxy.ts +0 -36
  155. package/src/session.ts +0 -129
  156. package/src/stripe.ts +0 -497
  157. package/src/telemetry.ts +0 -71
  158. package/src/test.ts +0 -135
  159. package/src/types/convex-api.d.ts +0 -20
  160. package/src/types/convex-api.ts +0 -21
  161. package/src/types.ts +0 -109
  162. package/src/ui/colors.ts +0 -219
  163. package/src/ui/errors.ts +0 -394
  164. package/src/ui/index.ts +0 -17
  165. package/src/ui/prompts.ts +0 -390
  166. package/src/ui/spinner.ts +0 -325
  167. package/src/utils/backup.ts +0 -224
  168. package/src/utils/config.ts +0 -318
  169. package/src/utils/os.ts +0 -124
  170. package/src/utils/paths.ts +0 -203
  171. package/src/webhook.ts +0 -107
  172. package/test-10-working.cjs +0 -97
  173. package/test-14-final.cjs +0 -96
  174. package/test-actual-handlers.ts +0 -92
  175. package/test-apilayer-all-14.ts +0 -249
  176. package/test-apilayer-fixed.ts +0 -248
  177. package/test-direct-endpoints.ts +0 -174
  178. package/test-exact-endpoints.ts +0 -144
  179. package/test-final.ts +0 -83
  180. package/test-full-routing.ts +0 -100
  181. package/test-handlers-correct.ts +0 -217
  182. package/test-numverify-key.ts +0 -41
  183. package/test-via-handlers.ts +0 -92
  184. package/test-worldnews.mjs +0 -26
  185. package/tsconfig.json +0 -20
package/convex/teams.ts DELETED
@@ -1,243 +0,0 @@
1
- import { v } from "convex/values";
2
- import { mutation, query } from "./_generated/server";
3
-
4
- // ============================================
5
- // HELPER FUNCTIONS
6
- // ============================================
7
-
8
- function generateToken(): string {
9
- const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
10
- let result = "";
11
- for (let i = 0; i < 32; i++) {
12
- result += chars.charAt(Math.floor(Math.random() * chars.length));
13
- }
14
- return result;
15
- }
16
-
17
- // ============================================
18
- // TEAM QUERIES
19
- // ============================================
20
-
21
- /**
22
- * Get team members for a workspace
23
- */
24
- export const getMembers = query({
25
- args: { token: v.string() },
26
- handler: async (ctx, { token }) => {
27
- const session = await ctx.db
28
- .query("agentSessions")
29
- .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
30
- .first();
31
-
32
- if (!session) return [];
33
-
34
- const workspace = await ctx.db.get(session.workspaceId);
35
- if (!workspace) return [];
36
-
37
- const members = await ctx.db
38
- .query("workspaceMembers")
39
- .withIndex("by_workspaceId", (q) => q.eq("workspaceId", session.workspaceId))
40
- .collect();
41
-
42
- // Add owner as first member
43
- return [
44
- {
45
- email: workspace.email,
46
- role: "owner" as const,
47
- status: "active" as const,
48
- isOwner: true,
49
- createdAt: workspace.createdAt,
50
- },
51
- ...members.map((m) => ({
52
- id: m._id,
53
- email: m.email,
54
- role: m.role,
55
- status: m.status,
56
- isOwner: false,
57
- invitedBy: m.invitedBy,
58
- createdAt: m.createdAt,
59
- acceptedAt: m.acceptedAt,
60
- })),
61
- ];
62
- },
63
- });
64
-
65
- // ============================================
66
- // TEAM MUTATIONS
67
- // ============================================
68
-
69
- /**
70
- * Invite a member to the workspace (creates pending invite)
71
- */
72
- export const inviteMember = mutation({
73
- args: {
74
- token: v.string(),
75
- email: v.string(),
76
- role: v.union(v.literal("admin"), v.literal("member")),
77
- },
78
- handler: async (ctx, { token, email, role }) => {
79
- const session = await ctx.db
80
- .query("agentSessions")
81
- .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
82
- .first();
83
-
84
- if (!session) throw new Error("Invalid session");
85
-
86
- const workspace = await ctx.db.get(session.workspaceId);
87
- if (!workspace) throw new Error("Workspace not found");
88
-
89
- // Normalize email
90
- const normalizedEmail = email.toLowerCase().trim();
91
-
92
- // Can't invite yourself
93
- if (normalizedEmail === workspace.email.toLowerCase()) {
94
- throw new Error("Cannot invite yourself");
95
- }
96
-
97
- // Check if already member
98
- const existing = await ctx.db
99
- .query("workspaceMembers")
100
- .withIndex("by_workspaceId_email", (q) =>
101
- q.eq("workspaceId", session.workspaceId).eq("email", normalizedEmail)
102
- )
103
- .first();
104
-
105
- if (existing) {
106
- if (existing.status === "active") {
107
- throw new Error("Already a team member");
108
- }
109
- if (existing.status === "pending") {
110
- throw new Error("Invite already pending");
111
- }
112
- // If revoked, we can re-invite - update existing record
113
- const inviteToken = generateToken();
114
- await ctx.db.patch(existing._id, {
115
- role,
116
- invitedBy: workspace.email,
117
- inviteToken,
118
- status: "pending",
119
- createdAt: Date.now(),
120
- acceptedAt: undefined,
121
- });
122
- return { id: existing._id, inviteToken };
123
- }
124
-
125
- // Generate invite token
126
- const inviteToken = generateToken();
127
-
128
- const id = await ctx.db.insert("workspaceMembers", {
129
- workspaceId: session.workspaceId,
130
- email: normalizedEmail,
131
- role,
132
- invitedBy: workspace.email,
133
- inviteToken,
134
- status: "pending",
135
- createdAt: Date.now(),
136
- });
137
-
138
- return { id, inviteToken };
139
- },
140
- });
141
-
142
- /**
143
- * Accept an invite
144
- */
145
- export const acceptInvite = mutation({
146
- args: { inviteToken: v.string() },
147
- handler: async (ctx, { inviteToken }) => {
148
- const member = await ctx.db
149
- .query("workspaceMembers")
150
- .withIndex("by_inviteToken", (q) => q.eq("inviteToken", inviteToken))
151
- .first();
152
-
153
- if (!member) throw new Error("Invalid invite token");
154
- if (member.status !== "pending") throw new Error("Invite already used or revoked");
155
-
156
- await ctx.db.patch(member._id, {
157
- status: "active",
158
- acceptedAt: Date.now(),
159
- inviteToken: undefined, // Clear token after use
160
- });
161
-
162
- // Get workspace info for response
163
- const workspace = await ctx.db.get(member.workspaceId);
164
-
165
- return {
166
- success: true,
167
- workspaceId: member.workspaceId,
168
- workspaceEmail: workspace?.email,
169
- role: member.role,
170
- };
171
- },
172
- });
173
-
174
- /**
175
- * Remove a member from the workspace
176
- */
177
- export const removeMember = mutation({
178
- args: {
179
- token: v.string(),
180
- memberEmail: v.string(),
181
- },
182
- handler: async (ctx, { token, memberEmail }) => {
183
- const session = await ctx.db
184
- .query("agentSessions")
185
- .withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
186
- .first();
187
-
188
- if (!session) throw new Error("Invalid session");
189
-
190
- const workspace = await ctx.db.get(session.workspaceId);
191
- if (!workspace) throw new Error("Workspace not found");
192
-
193
- const normalizedEmail = memberEmail.toLowerCase().trim();
194
-
195
- // Cannot remove owner
196
- if (normalizedEmail === workspace.email.toLowerCase()) {
197
- throw new Error("Cannot remove workspace owner");
198
- }
199
-
200
- const member = await ctx.db
201
- .query("workspaceMembers")
202
- .withIndex("by_workspaceId_email", (q) =>
203
- q.eq("workspaceId", session.workspaceId).eq("email", normalizedEmail)
204
- )
205
- .first();
206
-
207
- if (!member) throw new Error("Member not found");
208
-
209
- // Set status to revoked instead of deleting
210
- await ctx.db.patch(member._id, {
211
- status: "revoked",
212
- inviteToken: undefined,
213
- });
214
-
215
- return { success: true };
216
- },
217
- });
218
-
219
- /**
220
- * Get invite details (public, for invite acceptance page)
221
- */
222
- export const getInviteDetails = query({
223
- args: { inviteToken: v.string() },
224
- handler: async (ctx, { inviteToken }) => {
225
- const member = await ctx.db
226
- .query("workspaceMembers")
227
- .withIndex("by_inviteToken", (q) => q.eq("inviteToken", inviteToken))
228
- .first();
229
-
230
- if (!member) return null;
231
- if (member.status !== "pending") return null;
232
-
233
- const workspace = await ctx.db.get(member.workspaceId);
234
- if (!workspace) return null;
235
-
236
- return {
237
- email: member.email,
238
- role: member.role,
239
- invitedBy: member.invitedBy,
240
- workspaceEmail: workspace.email,
241
- };
242
- },
243
- });
@@ -1,81 +0,0 @@
1
- import { mutation, query } from "./_generated/server";
2
- import { v } from "convex/values";
3
-
4
- export const track = mutation({
5
- args: {
6
- event: v.object({
7
- type: v.string(),
8
- query: v.optional(v.string()),
9
- apiId: v.optional(v.string()),
10
- resultCount: v.optional(v.number()),
11
- responseTimeMs: v.optional(v.number()),
12
- version: v.optional(v.string()),
13
- platform: v.optional(v.string()),
14
- nodeVersion: v.optional(v.string()),
15
- timestamp: v.optional(v.number()),
16
- }),
17
- },
18
- handler: async (ctx, { event }) => {
19
- await ctx.db.insert("telemetry", {
20
- type: event.type,
21
- query: event.query,
22
- apiId: event.apiId,
23
- resultCount: event.resultCount,
24
- responseTimeMs: event.responseTimeMs,
25
- version: event.version || "unknown",
26
- platform: event.platform || "unknown",
27
- nodeVersion: event.nodeVersion || "unknown",
28
- timestamp: event.timestamp || Date.now(),
29
- });
30
- },
31
- });
32
-
33
- export const getStats = query({
34
- args: {},
35
- handler: async (ctx) => {
36
- const events = await ctx.db.query("telemetry").collect();
37
-
38
- const startups = events.filter(e => e.type === "startup").length;
39
- const searches = events.filter(e => e.type === "search").length;
40
- const executes = events.filter(e => e.type === "execute").length;
41
-
42
- const uniqueUsers = new Set(events.map(e => `${e.platform}-${e.nodeVersion}`)).size;
43
-
44
- const topQueries = events
45
- .filter(e => e.type === "search" && e.query)
46
- .reduce((acc, e) => {
47
- acc[e.query!] = (acc[e.query!] || 0) + 1;
48
- return acc;
49
- }, {} as Record<string, number>);
50
-
51
- const topAPIs = events
52
- .filter(e => e.type === "execute" && e.apiId)
53
- .reduce((acc, e) => {
54
- acc[e.apiId!] = (acc[e.apiId!] || 0) + 1;
55
- return acc;
56
- }, {} as Record<string, number>);
57
-
58
- return {
59
- totalStartups: startups,
60
- totalSearches: searches,
61
- totalExecutes: executes,
62
- estimatedUniqueUsers: uniqueUsers,
63
- topQueries: Object.entries(topQueries)
64
- .sort(([,a], [,b]) => b - a)
65
- .slice(0, 10),
66
- topAPIs: Object.entries(topAPIs)
67
- .sort(([,a], [,b]) => b - a)
68
- .slice(0, 10),
69
- };
70
- },
71
- });
72
-
73
- export const getRecent = query({
74
- args: { limit: v.optional(v.number()) },
75
- handler: async (ctx, { limit = 50 }) => {
76
- return await ctx.db
77
- .query("telemetry")
78
- .order("desc")
79
- .take(limit);
80
- },
81
- });
@@ -1,25 +0,0 @@
1
- {
2
- /* This TypeScript project config describes the environment that
3
- * Convex functions run in and is used to typecheck them.
4
- * You can modify it, but some settings are required to use Convex.
5
- */
6
- "compilerOptions": {
7
- /* These settings are not required by Convex and can be modified. */
8
- "allowJs": true,
9
- "strict": true,
10
- "moduleResolution": "Bundler",
11
- "jsx": "react-jsx",
12
- "skipLibCheck": true,
13
- "allowSyntheticDefaultImports": true,
14
-
15
- /* These compiler options are required by Convex */
16
- "target": "ESNext",
17
- "lib": ["ES2021", "dom"],
18
- "forceConsistentCasingInFileNames": true,
19
- "module": "ESNext",
20
- "isolatedModules": true,
21
- "noEmit": true
22
- },
23
- "include": ["./**/*"],
24
- "exclude": ["./_generated"]
25
- }
@@ -1,44 +0,0 @@
1
- import { mutation } from "./_generated/server";
2
-
3
- /**
4
- * Update providerAPIs status to reflect actual Direct Call status
5
- * Run: npx convex run updateAPIStatus:update
6
- */
7
- export const update = mutation({
8
- args: {},
9
- handler: async (ctx) => {
10
- const providerId = "k97cvcvadnyz8x8m4we7xqmh1s83p0ph" as any; // APILayer
11
-
12
- const apis = await ctx.db
13
- .query("providerAPIs")
14
- .withIndex("by_providerId", (q: any) => q.eq("providerId", providerId))
15
- .collect();
16
-
17
- // APIs that are blocked by subscription tier
18
- const blocked = ["Number Verification API", "World News API", "Image Crop API", "Form API"];
19
- // PDF Layer is rate limited
20
- const rateLimited = ["PDF Layer"];
21
-
22
- let updated = 0;
23
- for (const api of apis) {
24
- let newStatus = "approved"; // default = live
25
- let hasDirectCall = true;
26
-
27
- if (blocked.includes(api.name)) {
28
- newStatus = "blocked";
29
- hasDirectCall = false;
30
- } else if (rateLimited.includes(api.name)) {
31
- newStatus = "rate_limited";
32
- }
33
-
34
- if (api.status !== newStatus || (api as any).hasDirectCall !== hasDirectCall) {
35
- await ctx.db.patch(api._id, {
36
- status: newStatus,
37
- } as any);
38
- updated++;
39
- }
40
- }
41
-
42
- return { success: true, updated, total: apis.length };
43
- },
44
- });
package/convex/usage.ts DELETED
@@ -1,260 +0,0 @@
1
- import { v } from "convex/values";
2
- import { mutation, query } from "./_generated/server";
3
-
4
- // ============================================
5
- // MUTATIONS
6
- // ============================================
7
-
8
- /**
9
- * Log an API call usage
10
- */
11
- export const logUsage = mutation({
12
- args: {
13
- userId: v.string(),
14
- providerId: v.id("providers"),
15
- directCallId: v.id("providerDirectCall"),
16
- actionName: v.string(),
17
- success: v.boolean(),
18
- latencyMs: v.number(),
19
- creditsUsed: v.number(),
20
- errorMessage: v.optional(v.string()),
21
- },
22
- handler: async (ctx, args) => {
23
- return await ctx.db.insert("usageLog", {
24
- userId: args.userId,
25
- providerId: args.providerId,
26
- directCallId: args.directCallId,
27
- actionName: args.actionName,
28
- timestamp: Date.now(),
29
- success: args.success,
30
- latencyMs: args.latencyMs,
31
- creditsUsed: args.creditsUsed,
32
- errorMessage: args.errorMessage,
33
- });
34
- },
35
- });
36
-
37
- // ============================================
38
- // QUERIES
39
- // ============================================
40
-
41
- /**
42
- * Get user usage stats for rate limiting
43
- * Returns counts for last minute and last day
44
- */
45
- export const getUserUsage = query({
46
- args: {
47
- userId: v.string(),
48
- providerId: v.id("providers"),
49
- },
50
- handler: async (ctx, args) => {
51
- const now = Date.now();
52
- const oneMinuteAgo = now - 60 * 1000;
53
- const oneDayAgo = now - 24 * 60 * 60 * 1000;
54
-
55
- // Get all usage for this user + provider in the last 24h
56
- const recentUsage = await ctx.db
57
- .query("usageLog")
58
- .withIndex("by_userId_providerId", (q) =>
59
- q.eq("userId", args.userId).eq("providerId", args.providerId)
60
- )
61
- .filter((q) => q.gte(q.field("timestamp"), oneDayAgo))
62
- .collect();
63
-
64
- // Calculate counts
65
- const minuteCount = recentUsage.filter((u) => u.timestamp >= oneMinuteAgo).length;
66
- const dayCount = recentUsage.length;
67
- const totalCredits = recentUsage.reduce((sum, u) => sum + u.creditsUsed, 0);
68
-
69
- return {
70
- minute: minuteCount,
71
- day: dayCount,
72
- totalCreditsUsed: totalCredits,
73
- };
74
- },
75
- });
76
-
77
- /**
78
- * Get provider usage stats for analytics
79
- */
80
- export const getProviderUsage = query({
81
- args: {
82
- providerId: v.id("providers"),
83
- periodDays: v.optional(v.number()), // default 30
84
- },
85
- handler: async (ctx, args) => {
86
- const periodDays = args.periodDays ?? 30;
87
- const now = Date.now();
88
- const periodStart = now - periodDays * 24 * 60 * 60 * 1000;
89
-
90
- const usage = await ctx.db
91
- .query("usageLog")
92
- .withIndex("by_providerId", (q) => q.eq("providerId", args.providerId))
93
- .filter((q) => q.gte(q.field("timestamp"), periodStart))
94
- .collect();
95
-
96
- // Aggregate stats
97
- const totalCalls = usage.length;
98
- const successfulCalls = usage.filter((u) => u.success).length;
99
- const failedCalls = totalCalls - successfulCalls;
100
- const totalCredits = usage.reduce((sum, u) => sum + u.creditsUsed, 0);
101
- const totalLatency = usage.reduce((sum, u) => sum + u.latencyMs, 0);
102
- const avgLatency = totalCalls > 0 ? Math.round(totalLatency / totalCalls) : 0;
103
-
104
- // Group by action
105
- const byAction: Record<string, { calls: number; credits: number }> = {};
106
- for (const u of usage) {
107
- if (!byAction[u.actionName]) {
108
- byAction[u.actionName] = { calls: 0, credits: 0 };
109
- }
110
- byAction[u.actionName].calls++;
111
- byAction[u.actionName].credits += u.creditsUsed;
112
- }
113
-
114
- // Group by day for chart
115
- const byDay: Record<string, { calls: number; credits: number }> = {};
116
- for (const u of usage) {
117
- const day = new Date(u.timestamp).toISOString().split("T")[0];
118
- if (!byDay[day]) {
119
- byDay[day] = { calls: 0, credits: 0 };
120
- }
121
- byDay[day].calls++;
122
- byDay[day].credits += u.creditsUsed;
123
- }
124
-
125
- // Unique users
126
- const uniqueUsers = new Set(usage.map((u) => u.userId)).size;
127
-
128
- return {
129
- periodDays,
130
- totalCalls,
131
- successfulCalls,
132
- failedCalls,
133
- successRate: totalCalls > 0 ? (successfulCalls / totalCalls) * 100 : 0,
134
- totalCredits,
135
- avgLatencyMs: avgLatency,
136
- uniqueUsers,
137
- byAction,
138
- byDay,
139
- };
140
- },
141
- });
142
-
143
- /**
144
- * Get Direct Call specific usage stats
145
- */
146
- export const getDirectCallUsage = query({
147
- args: {
148
- directCallId: v.id("providerDirectCall"),
149
- periodDays: v.optional(v.number()),
150
- },
151
- handler: async (ctx, args) => {
152
- const periodDays = args.periodDays ?? 30;
153
- const now = Date.now();
154
- const periodStart = now - periodDays * 24 * 60 * 60 * 1000;
155
-
156
- const usage = await ctx.db
157
- .query("usageLog")
158
- .withIndex("by_directCallId", (q) => q.eq("directCallId", args.directCallId))
159
- .filter((q) => q.gte(q.field("timestamp"), periodStart))
160
- .collect();
161
-
162
- const totalCalls = usage.length;
163
- const successfulCalls = usage.filter((u) => u.success).length;
164
- const totalCredits = usage.reduce((sum, u) => sum + u.creditsUsed, 0);
165
- const totalLatency = usage.reduce((sum, u) => sum + u.latencyMs, 0);
166
-
167
- return {
168
- periodDays,
169
- totalCalls,
170
- successfulCalls,
171
- failedCalls: totalCalls - successfulCalls,
172
- successRate: totalCalls > 0 ? (successfulCalls / totalCalls) * 100 : 0,
173
- totalCredits,
174
- avgLatencyMs: totalCalls > 0 ? Math.round(totalLatency / totalCalls) : 0,
175
- uniqueUsers: new Set(usage.map((u) => u.userId)).size,
176
- };
177
- },
178
- });
179
-
180
- /**
181
- * Get recent usage logs (for dashboard/debugging)
182
- */
183
- export const getRecentLogs = query({
184
- args: {
185
- providerId: v.optional(v.id("providers")),
186
- directCallId: v.optional(v.id("providerDirectCall")),
187
- limit: v.optional(v.number()),
188
- },
189
- handler: async (ctx, args) => {
190
- const limit = args.limit ?? 50;
191
- const { directCallId, providerId } = args;
192
-
193
- if (directCallId !== undefined) {
194
- return await ctx.db
195
- .query("usageLog")
196
- .withIndex("by_directCallId", (q) => q.eq("directCallId", directCallId))
197
- .order("desc")
198
- .take(limit);
199
- }
200
-
201
- if (providerId !== undefined) {
202
- return await ctx.db
203
- .query("usageLog")
204
- .withIndex("by_providerId", (q) => q.eq("providerId", providerId))
205
- .order("desc")
206
- .take(limit);
207
- }
208
-
209
- return await ctx.db
210
- .query("usageLog")
211
- .withIndex("by_timestamp")
212
- .order("desc")
213
- .take(limit);
214
- },
215
- });
216
-
217
- /**
218
- * Check if user is within rate limits
219
- * Returns { allowed: boolean, reason?: string }
220
- */
221
- export const checkRateLimit = query({
222
- args: {
223
- userId: v.string(),
224
- providerId: v.id("providers"),
225
- rateLimitPerUser: v.number(),
226
- rateLimitPerDay: v.number(),
227
- },
228
- handler: async (ctx, args) => {
229
- const now = Date.now();
230
- const oneMinuteAgo = now - 60 * 1000;
231
- const oneDayAgo = now - 24 * 60 * 60 * 1000;
232
-
233
- const recentUsage = await ctx.db
234
- .query("usageLog")
235
- .withIndex("by_userId_providerId", (q) =>
236
- q.eq("userId", args.userId).eq("providerId", args.providerId)
237
- )
238
- .filter((q) => q.gte(q.field("timestamp"), oneDayAgo))
239
- .collect();
240
-
241
- const minuteCount = recentUsage.filter((u) => u.timestamp >= oneMinuteAgo).length;
242
- const dayCount = recentUsage.length;
243
-
244
- if (minuteCount >= args.rateLimitPerUser) {
245
- return {
246
- allowed: false,
247
- reason: `Rate limit exceeded: ${minuteCount}/${args.rateLimitPerUser} requests per minute`,
248
- };
249
- }
250
-
251
- if (dayCount >= args.rateLimitPerDay) {
252
- return {
253
- allowed: false,
254
- reason: `Daily limit exceeded: ${dayCount}/${args.rateLimitPerDay} requests per day`,
255
- };
256
- }
257
-
258
- return { allowed: true };
259
- },
260
- });