@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/billing.ts DELETED
@@ -1,834 +0,0 @@
1
- import { v } from "convex/values";
2
- import { mutation, query, internalMutation, internalAction, internalQuery } from "./_generated/server";
3
- import { Id } from "./_generated/dataModel";
4
- import { internal } from "./_generated/api";
5
- import Stripe from "stripe";
6
-
7
- // Initialize Stripe
8
- function getStripe(): Stripe {
9
- const key = process.env.STRIPE_SECRET_KEY;
10
- if (!key) {
11
- throw new Error("STRIPE_SECRET_KEY not configured");
12
- }
13
- return new Stripe(key);
14
- }
15
-
16
- // ============================================
17
- // MUTATIONS
18
- // ============================================
19
-
20
- /**
21
- * Link a Stripe customer to a workspace
22
- */
23
- export const linkCustomer = mutation({
24
- args: {
25
- workspaceId: v.id("workspaces"),
26
- stripeCustomerId: v.string(),
27
- },
28
- handler: async (ctx, args) => {
29
- const workspace = await ctx.db.get(args.workspaceId);
30
- if (!workspace) {
31
- throw new Error("Workspace not found");
32
- }
33
-
34
- await ctx.db.patch(args.workspaceId, {
35
- stripeCustomerId: args.stripeCustomerId,
36
- updatedAt: Date.now(),
37
- });
38
-
39
- return { success: true };
40
- },
41
- });
42
-
43
- /**
44
- * Update subscription status for a workspace
45
- */
46
- export const updateSubscription = mutation({
47
- args: {
48
- workspaceId: v.id("workspaces"),
49
- stripeSubscriptionId: v.optional(v.string()),
50
- billingPlan: v.string(),
51
- },
52
- handler: async (ctx, args) => {
53
- const workspace = await ctx.db.get(args.workspaceId);
54
- if (!workspace) {
55
- throw new Error("Workspace not found");
56
- }
57
-
58
- // Update tier and usage limit based on plan
59
- const planLimits: Record<string, number> = {
60
- free: 50,
61
- usage_based: 999999999, // Effectively unlimited
62
- starter: 1000,
63
- pro: 10000,
64
- scale: 100000,
65
- };
66
-
67
- const newLimit = planLimits[args.billingPlan] || 50;
68
-
69
- // Determine tier
70
- let newTier = args.billingPlan === "free" ? "free" : args.billingPlan;
71
-
72
- await ctx.db.patch(args.workspaceId, {
73
- stripeSubscriptionId: args.stripeSubscriptionId,
74
- billingPlan: args.billingPlan,
75
- tier: newTier,
76
- usageLimit: newLimit,
77
- updatedAt: Date.now(),
78
- });
79
-
80
- return { success: true, newLimit };
81
- },
82
- });
83
-
84
- /**
85
- * Record daily usage for billing
86
- */
87
- export const recordUsage = mutation({
88
- args: {
89
- workspaceId: v.id("workspaces"),
90
- callCount: v.number(),
91
- date: v.string(), // "2026-02-28" format
92
- },
93
- handler: async (ctx, args) => {
94
- // Check if record exists for this date
95
- const existing = await ctx.db
96
- .query("usageRecords")
97
- .withIndex("by_workspaceId_date", (q) =>
98
- q.eq("workspaceId", args.workspaceId).eq("date", args.date)
99
- )
100
- .unique();
101
-
102
- if (existing) {
103
- // Update existing record
104
- await ctx.db.patch(existing._id, {
105
- callCount: existing.callCount + args.callCount,
106
- updatedAt: Date.now(),
107
- });
108
- return { id: existing._id, callCount: existing.callCount + args.callCount };
109
- } else {
110
- // Create new record
111
- const id = await ctx.db.insert("usageRecords", {
112
- workspaceId: args.workspaceId,
113
- date: args.date,
114
- callCount: args.callCount,
115
- reportedToStripe: false,
116
- createdAt: Date.now(),
117
- updatedAt: Date.now(),
118
- });
119
- return { id, callCount: args.callCount };
120
- }
121
- },
122
- });
123
-
124
- /**
125
- * Log actual cost for a single API call (called by gateway after response)
126
- * Accumulates into daily usageRecords for Stripe reporting
127
- */
128
- export const logCallCost = internalMutation({
129
- args: {
130
- workspaceId: v.id("workspaces"),
131
- provider: v.string(),
132
- model: v.string(),
133
- providerCostUsd: v.float64(),
134
- apiclawCostUsd: v.float64(),
135
- inputTokens: v.number(),
136
- outputTokens: v.number(),
137
- },
138
- handler: async (ctx, args) => {
139
- const date = new Date().toISOString().split("T")[0]; // "2026-04-11"
140
-
141
- const existing = await ctx.db
142
- .query("usageRecords")
143
- .withIndex("by_workspaceId_date", (q) =>
144
- q.eq("workspaceId", args.workspaceId).eq("date", date)
145
- )
146
- .unique();
147
-
148
- if (existing) {
149
- await ctx.db.patch(existing._id, {
150
- callCount: existing.callCount + 1,
151
- providerCostUsd: (existing.providerCostUsd || 0) + args.providerCostUsd,
152
- apiclawCostUsd: (existing.apiclawCostUsd || 0) + args.apiclawCostUsd,
153
- updatedAt: Date.now(),
154
- });
155
- } else {
156
- await ctx.db.insert("usageRecords", {
157
- workspaceId: args.workspaceId,
158
- date,
159
- callCount: 1,
160
- providerCostUsd: args.providerCostUsd,
161
- apiclawCostUsd: args.apiclawCostUsd,
162
- reportedToStripe: false,
163
- createdAt: Date.now(),
164
- updatedAt: Date.now(),
165
- });
166
- }
167
- },
168
- });
169
-
170
- /**
171
- * Process a successful payment (from webhook)
172
- */
173
- export const processPayment = mutation({
174
- args: {
175
- stripeInvoiceId: v.string(),
176
- workspaceId: v.id("workspaces"),
177
- amount: v.number(), // in cents
178
- periodStart: v.number(),
179
- periodEnd: v.number(),
180
- callCount: v.number(),
181
- pdfUrl: v.optional(v.string()),
182
- },
183
- handler: async (ctx, args) => {
184
- // Check for idempotency - don't process same invoice twice
185
- const existing = await ctx.db
186
- .query("invoices")
187
- .withIndex("by_stripeInvoiceId", (q) =>
188
- q.eq("stripeInvoiceId", args.stripeInvoiceId)
189
- )
190
- .unique();
191
-
192
- if (existing) {
193
- // Already processed, return existing
194
- return { id: existing._id, alreadyProcessed: true };
195
- }
196
-
197
- // Create invoice record
198
- const id = await ctx.db.insert("invoices", {
199
- workspaceId: args.workspaceId,
200
- stripeInvoiceId: args.stripeInvoiceId,
201
- amount: args.amount,
202
- status: "paid",
203
- periodStart: args.periodStart,
204
- periodEnd: args.periodEnd,
205
- callCount: args.callCount,
206
- pdfUrl: args.pdfUrl,
207
- createdAt: Date.now(),
208
- });
209
-
210
- // Update workspace last billing date
211
- await ctx.db.patch(args.workspaceId, {
212
- lastBillingDate: Date.now(),
213
- updatedAt: Date.now(),
214
- });
215
-
216
- return { id, alreadyProcessed: false };
217
- },
218
- });
219
-
220
- /**
221
- * Increment credit balance (for prepaid credits)
222
- */
223
- export const incrementCredits = mutation({
224
- args: {
225
- workspaceId: v.id("workspaces"),
226
- amount: v.number(), // in cents
227
- },
228
- handler: async (ctx, args) => {
229
- const workspace = await ctx.db.get(args.workspaceId);
230
- if (!workspace) {
231
- throw new Error("Workspace not found");
232
- }
233
-
234
- const currentBalance = workspace.creditBalance || 0;
235
- const newBalance = currentBalance + args.amount;
236
-
237
- await ctx.db.patch(args.workspaceId, {
238
- creditBalance: newBalance,
239
- updatedAt: Date.now(),
240
- });
241
-
242
- return { previousBalance: currentBalance, newBalance };
243
- },
244
- });
245
-
246
- /**
247
- * Decrement credit balance (when using prepaid credits)
248
- */
249
- export const decrementCredits = mutation({
250
- args: {
251
- workspaceId: v.id("workspaces"),
252
- amount: v.number(), // in cents
253
- },
254
- handler: async (ctx, args) => {
255
- const workspace = await ctx.db.get(args.workspaceId);
256
- if (!workspace) {
257
- throw new Error("Workspace not found");
258
- }
259
-
260
- const currentBalance = workspace.creditBalance || 0;
261
- if (currentBalance < args.amount) {
262
- throw new Error("Insufficient credit balance");
263
- }
264
-
265
- const newBalance = currentBalance - args.amount;
266
-
267
- await ctx.db.patch(args.workspaceId, {
268
- creditBalance: newBalance,
269
- updatedAt: Date.now(),
270
- });
271
-
272
- return { previousBalance: currentBalance, newBalance };
273
- },
274
- });
275
-
276
- /**
277
- * Mark usage as reported to Stripe
278
- */
279
- export const markUsageReported = internalMutation({
280
- args: {
281
- usageRecordId: v.id("usageRecords"),
282
- stripeUsageRecordId: v.string(),
283
- },
284
- handler: async (ctx, args) => {
285
- await ctx.db.patch(args.usageRecordId, {
286
- reportedToStripe: true,
287
- stripeUsageRecordId: args.stripeUsageRecordId,
288
- updatedAt: Date.now(),
289
- });
290
- },
291
- });
292
-
293
- /**
294
- * Update invoice status (from webhook)
295
- */
296
- export const updateInvoiceStatus = mutation({
297
- args: {
298
- stripeInvoiceId: v.string(),
299
- status: v.string(),
300
- },
301
- handler: async (ctx, args) => {
302
- const invoice = await ctx.db
303
- .query("invoices")
304
- .withIndex("by_stripeInvoiceId", (q) =>
305
- q.eq("stripeInvoiceId", args.stripeInvoiceId)
306
- )
307
- .unique();
308
-
309
- if (!invoice) {
310
- return { found: false };
311
- }
312
-
313
- await ctx.db.patch(invoice._id, {
314
- status: args.status,
315
- });
316
-
317
- return { found: true, id: invoice._id };
318
- },
319
- });
320
-
321
- /**
322
- * Reset usage count on subscription cancellation
323
- * Gives user a clean slate when downgrading to free
324
- */
325
- export const resetUsageOnCancellation = mutation({
326
- args: {
327
- workspaceId: v.id("workspaces"),
328
- },
329
- handler: async (ctx, args) => {
330
- const workspace = await ctx.db.get(args.workspaceId);
331
- if (!workspace) {
332
- throw new Error("Workspace not found");
333
- }
334
-
335
- await ctx.db.patch(args.workspaceId, {
336
- usageCount: 0,
337
- updatedAt: Date.now(),
338
- });
339
-
340
- return { success: true, previousUsage: workspace.usageCount };
341
- },
342
- });
343
-
344
- /**
345
- * Update payment method info (from webhook)
346
- */
347
- export const updatePaymentMethodInfo = mutation({
348
- args: {
349
- workspaceId: v.id("workspaces"),
350
- hasPaymentMethod: v.boolean(),
351
- paymentMethodType: v.optional(v.string()),
352
- cardBrand: v.optional(v.string()),
353
- cardLast4: v.optional(v.string()),
354
- },
355
- handler: async (ctx, args) => {
356
- const workspace = await ctx.db.get(args.workspaceId);
357
- if (!workspace) {
358
- throw new Error("Workspace not found");
359
- }
360
-
361
- await ctx.db.patch(args.workspaceId, {
362
- hasPaymentMethod: args.hasPaymentMethod,
363
- paymentMethodType: args.paymentMethodType,
364
- cardBrand: args.cardBrand,
365
- cardLast4: args.cardLast4,
366
- updatedAt: Date.now(),
367
- });
368
-
369
- return { success: true };
370
- },
371
- });
372
-
373
- // ============================================
374
- // QUERIES
375
- // ============================================
376
-
377
- /**
378
- * Get billing info for a workspace
379
- */
380
- export const getInfo = query({
381
- args: {
382
- workspaceId: v.id("workspaces"),
383
- },
384
- handler: async (ctx, args) => {
385
- const workspace = await ctx.db.get(args.workspaceId);
386
- if (!workspace) {
387
- throw new Error("Workspace not found");
388
- }
389
-
390
- // Get recent invoices
391
- const invoices = await ctx.db
392
- .query("invoices")
393
- .withIndex("by_workspaceId_createdAt", (q) =>
394
- q.eq("workspaceId", args.workspaceId)
395
- )
396
- .order("desc")
397
- .take(10);
398
-
399
- // Calculate current period usage
400
- const now = new Date();
401
- const periodStart = new Date(now.getFullYear(), now.getMonth(), 1);
402
- const periodStartStr = periodStart.toISOString().split("T")[0];
403
-
404
- const usageRecords = await ctx.db
405
- .query("usageRecords")
406
- .withIndex("by_workspaceId", (q) => q.eq("workspaceId", args.workspaceId))
407
- .collect();
408
-
409
- const currentPeriodUsage = usageRecords
410
- .filter((r) => r.date >= periodStartStr)
411
- .reduce((sum, r) => sum + r.callCount, 0);
412
-
413
- // Determine plan limits
414
- const plan = workspace.billingPlan || "free";
415
- const planLimits: Record<string, number> = {
416
- free: 100,
417
- usage_based: 999999999,
418
- starter: 1000,
419
- pro: 10000,
420
- scale: 100000,
421
- };
422
-
423
- return {
424
- plan,
425
- tier: workspace.tier,
426
- usage: workspace.usageCount,
427
- currentPeriodUsage,
428
- limit: workspace.usageLimit,
429
- creditBalance: workspace.creditBalance || 0,
430
- stripeCustomerId: workspace.stripeCustomerId,
431
- stripeSubscriptionId: workspace.stripeSubscriptionId,
432
- lastBillingDate: workspace.lastBillingDate,
433
- invoices: invoices.map((inv) => ({
434
- id: inv._id,
435
- stripeInvoiceId: inv.stripeInvoiceId,
436
- amount: inv.amount,
437
- status: inv.status,
438
- periodStart: inv.periodStart,
439
- periodEnd: inv.periodEnd,
440
- callCount: inv.callCount,
441
- pdfUrl: inv.pdfUrl,
442
- createdAt: inv.createdAt,
443
- })),
444
- // Check if payment method needed
445
- needsPaymentMethod:
446
- plan === "free" && workspace.usageCount >= workspace.usageLimit,
447
- };
448
- },
449
- });
450
-
451
- /**
452
- * Get current period usage
453
- */
454
- export const getCurrentUsage = query({
455
- args: {
456
- workspaceId: v.id("workspaces"),
457
- },
458
- handler: async (ctx, args) => {
459
- const workspace = await ctx.db.get(args.workspaceId);
460
- if (!workspace) {
461
- throw new Error("Workspace not found");
462
- }
463
-
464
- // Current billing period (month)
465
- const now = new Date();
466
- const periodStart = new Date(now.getFullYear(), now.getMonth(), 1);
467
- const periodEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0);
468
- const periodStartStr = periodStart.toISOString().split("T")[0];
469
-
470
- // Get usage records for this period
471
- const usageRecords = await ctx.db
472
- .query("usageRecords")
473
- .withIndex("by_workspaceId", (q) => q.eq("workspaceId", args.workspaceId))
474
- .collect();
475
-
476
- const periodRecords = usageRecords.filter((r) => r.date >= periodStartStr);
477
- const callCount = periodRecords.reduce((sum, r) => sum + r.callCount, 0);
478
-
479
- // Calculate cost from actual tracked costs (or estimate from call count)
480
- const FREE_CALLS = 100;
481
- const totalApiclawCost = periodRecords.reduce((sum: number, r: any) => sum + (r.apiclawCostUsd || 0), 0);
482
- const billableCalls = Math.max(0, callCount - FREE_CALLS);
483
- // Use actual tracked cost if available, otherwise estimate at $0.01/call
484
- const estimatedCost = totalApiclawCost > 0
485
- ? Math.round(totalApiclawCost * 100) // convert USD to cents
486
- : billableCalls * 1;
487
-
488
- return {
489
- callCount,
490
- periodStart: periodStart.getTime(),
491
- periodEnd: periodEnd.getTime(),
492
- limit: workspace.usageLimit,
493
- remaining: Math.max(0, workspace.usageLimit - workspace.usageCount),
494
- percentUsed: Math.round((workspace.usageCount / workspace.usageLimit) * 100),
495
- billableCalls,
496
- estimatedCostCents: estimatedCost,
497
- dailyBreakdown: periodRecords.map((r) => ({
498
- date: r.date,
499
- calls: r.callCount,
500
- reportedToStripe: r.reportedToStripe,
501
- })),
502
- };
503
- },
504
- });
505
-
506
- /**
507
- * Get invoices for a workspace
508
- */
509
- export const getInvoices = query({
510
- args: {
511
- workspaceId: v.id("workspaces"),
512
- limit: v.optional(v.number()),
513
- },
514
- handler: async (ctx, args) => {
515
- const limit = args.limit || 20;
516
-
517
- const invoices = await ctx.db
518
- .query("invoices")
519
- .withIndex("by_workspaceId_createdAt", (q) =>
520
- q.eq("workspaceId", args.workspaceId)
521
- )
522
- .order("desc")
523
- .take(limit);
524
-
525
- return invoices.map((inv) => ({
526
- id: inv._id,
527
- stripeInvoiceId: inv.stripeInvoiceId,
528
- amount: inv.amount,
529
- amountFormatted: `$${(inv.amount / 100).toFixed(2)}`,
530
- status: inv.status,
531
- periodStart: inv.periodStart,
532
- periodEnd: inv.periodEnd,
533
- callCount: inv.callCount,
534
- pdfUrl: inv.pdfUrl,
535
- createdAt: inv.createdAt,
536
- }));
537
- },
538
- });
539
-
540
- /**
541
- * Get unreported usage records (for cron job)
542
- */
543
- export const getUnreportedUsage = query({
544
- args: {},
545
- handler: async (ctx) => {
546
- const unreported = await ctx.db
547
- .query("usageRecords")
548
- .withIndex("by_reportedToStripe", (q) => q.eq("reportedToStripe", false))
549
- .collect();
550
-
551
- return unreported;
552
- },
553
- });
554
-
555
- /**
556
- * Get workspace by Stripe customer ID
557
- */
558
- export const getByStripeCustomerId = query({
559
- args: {
560
- stripeCustomerId: v.string(),
561
- },
562
- handler: async (ctx, args) => {
563
- return await ctx.db
564
- .query("workspaces")
565
- .withIndex("by_stripeCustomerId", (q) =>
566
- q.eq("stripeCustomerId", args.stripeCustomerId)
567
- )
568
- .unique();
569
- },
570
- });
571
-
572
- /**
573
- * Get workspace by ID
574
- */
575
- export const getWorkspace = query({
576
- args: {
577
- id: v.id("workspaces"),
578
- },
579
- handler: async (ctx, args) => {
580
- return await ctx.db.get(args.id);
581
- },
582
- });
583
-
584
- // ============================================
585
- // STRIPE USAGE REPORTING (Internal Actions)
586
- // ============================================
587
-
588
- /**
589
- * Get all workspaces with active Stripe subscriptions (internal)
590
- */
591
- export const getActiveSubscriptions = internalQuery({
592
- args: {},
593
- handler: async (ctx) => {
594
- // Get all workspaces with a subscription ID
595
- const workspaces = await ctx.db
596
- .query("workspaces")
597
- .filter((q) => q.neq(q.field("stripeSubscriptionId"), undefined))
598
- .collect();
599
-
600
- // Filter to only those with billing plan that requires reporting
601
- return workspaces.filter(
602
- (w) => w.billingPlan === "usage_based" && w.stripeSubscriptionId
603
- );
604
- },
605
- });
606
-
607
- /**
608
- * Get unreported usage records for a specific workspace (internal)
609
- */
610
- export const getUnreportedUsageForWorkspace = internalQuery({
611
- args: {
612
- workspaceId: v.id("workspaces"),
613
- },
614
- handler: async (ctx, args) => {
615
- return await ctx.db
616
- .query("usageRecords")
617
- .withIndex("by_workspaceId", (q) => q.eq("workspaceId", args.workspaceId))
618
- .filter((q) => q.eq(q.field("reportedToStripe"), false))
619
- .collect();
620
- },
621
- });
622
-
623
- /**
624
- * Mark multiple usage records as reported (internal)
625
- */
626
- export const markUsageRecordsReported = internalMutation({
627
- args: {
628
- usageRecordIds: v.array(v.id("usageRecords")),
629
- stripeUsageRecordId: v.string(),
630
- },
631
- handler: async (ctx, args) => {
632
- const now = Date.now();
633
- for (const recordId of args.usageRecordIds) {
634
- await ctx.db.patch(recordId, {
635
- reportedToStripe: true,
636
- stripeUsageRecordId: args.stripeUsageRecordId,
637
- updatedAt: now,
638
- });
639
- }
640
- },
641
- });
642
-
643
- /**
644
- * Report usage to Stripe for a single workspace
645
- * Internal action - called by the daily cron
646
- */
647
- export const reportUsageToStripe = internalAction({
648
- args: {
649
- workspaceId: v.id("workspaces"),
650
- stripeSubscriptionId: v.string(),
651
- },
652
- handler: async (ctx, args): Promise<{ success: boolean; callCount: number; error?: string }> => {
653
- try {
654
- // Get unreported usage records
655
- const usageRecords = await ctx.runQuery(
656
- internal.billing.getUnreportedUsageForWorkspace,
657
- { workspaceId: args.workspaceId }
658
- );
659
-
660
- if (usageRecords.length === 0) {
661
- return { success: true, callCount: 0 };
662
- }
663
-
664
- // Sum up all unreported calls and costs
665
- const totalCalls = usageRecords.reduce(
666
- (sum: number, r: { callCount: number }) => sum + r.callCount,
667
- 0
668
- );
669
- const totalCostUsd = usageRecords.reduce(
670
- (sum: number, r: any) => sum + (r.apiclawCostUsd || r.callCount * 0.002),
671
- 0
672
- );
673
-
674
- if (totalCalls === 0) {
675
- // Mark as reported even if 0 calls (to prevent re-processing)
676
- await ctx.runMutation(internal.billing.markUsageRecordsReported, {
677
- usageRecordIds: usageRecords.map((r: { _id: Id<"usageRecords"> }) => r._id),
678
- stripeUsageRecordId: "zero_usage",
679
- });
680
- return { success: true, callCount: 0 };
681
- }
682
-
683
- const stripe = getStripe();
684
-
685
- // Get subscription to find the metered subscription item
686
- const subscription = await stripe.subscriptions.retrieve(args.stripeSubscriptionId);
687
-
688
- // Find the metered price item (usage_based price)
689
- const meteredItem = subscription.items.data.find((item) => {
690
- return item.price.recurring?.usage_type === "metered";
691
- });
692
-
693
- if (!meteredItem) {
694
- console.error(
695
- `No metered subscription item found for subscription ${args.stripeSubscriptionId}`
696
- );
697
- return {
698
- success: false,
699
- callCount: totalCalls,
700
- error: "No metered subscription item found",
701
- };
702
- }
703
-
704
- // Report usage to Stripe using usage records API
705
- // Quantity = cost in cents (rounded up to nearest cent)
706
- // Stripe price should be set to $0.01 per unit (1 cent per unit)
707
- const costInCents = Math.ceil(totalCostUsd * 100);
708
- const stripeKey = process.env.STRIPE_SECRET_KEY;
709
- const usageRecordResponse = await fetch(
710
- `https://api.stripe.com/v1/subscription_items/${meteredItem.id}/usage_records`,
711
- {
712
- method: "POST",
713
- headers: {
714
- "Authorization": `Bearer ${stripeKey}`,
715
- "Content-Type": "application/x-www-form-urlencoded",
716
- },
717
- body: new URLSearchParams({
718
- quantity: String(costInCents),
719
- timestamp: String(Math.floor(Date.now() / 1000)),
720
- action: "increment",
721
- }),
722
- }
723
- );
724
-
725
- if (!usageRecordResponse.ok) {
726
- const errorData = await usageRecordResponse.text();
727
- throw new Error(`Stripe API error: ${usageRecordResponse.status} - ${errorData}`);
728
- }
729
-
730
- const usageRecord = await usageRecordResponse.json() as { id: string };
731
-
732
- // Mark records as reported
733
- await ctx.runMutation(internal.billing.markUsageRecordsReported, {
734
- usageRecordIds: usageRecords.map((r: { _id: Id<"usageRecords"> }) => r._id),
735
- stripeUsageRecordId: usageRecord.id,
736
- });
737
-
738
- console.log(
739
- `Reported ${totalCalls} calls for workspace ${args.workspaceId} to Stripe`
740
- );
741
-
742
- return { success: true, callCount: totalCalls };
743
- } catch (error: any) {
744
- console.error(
745
- `Failed to report usage for workspace ${args.workspaceId}:`,
746
- error
747
- );
748
- return {
749
- success: false,
750
- callCount: 0,
751
- error: error.message || "Unknown error",
752
- };
753
- }
754
- },
755
- });
756
-
757
- // Type for workspace with active subscription
758
- type ActiveWorkspace = {
759
- _id: Id<"workspaces">;
760
- email: string;
761
- stripeSubscriptionId?: string;
762
- };
763
-
764
- // Type for cron results
765
- type CronResults = {
766
- total: number;
767
- success: number;
768
- failed: number;
769
- skipped: number;
770
- totalCallsReported: number;
771
- errors: string[];
772
- };
773
-
774
- /**
775
- * Daily cron job: Report all unreported usage to Stripe
776
- * Runs at 00:05 UTC
777
- */
778
- export const reportAllUsageToStripe = internalAction({
779
- args: {},
780
- handler: async (ctx): Promise<CronResults> => {
781
- console.log("[Cron] Starting daily usage reporting to Stripe...");
782
-
783
- // Get all workspaces with active subscriptions
784
- const workspaces: ActiveWorkspace[] = await ctx.runQuery(
785
- internal.billing.getActiveSubscriptions,
786
- {}
787
- );
788
-
789
- console.log(`[Cron] Found ${workspaces.length} workspaces with active subscriptions`);
790
-
791
- const results: CronResults = {
792
- total: workspaces.length,
793
- success: 0,
794
- failed: 0,
795
- skipped: 0,
796
- totalCallsReported: 0,
797
- errors: [],
798
- };
799
-
800
- // Process each workspace
801
- for (const workspace of workspaces) {
802
- if (!workspace.stripeSubscriptionId) {
803
- results.skipped++;
804
- continue;
805
- }
806
-
807
- try {
808
- const result = await ctx.runAction(internal.billing.reportUsageToStripe, {
809
- workspaceId: workspace._id,
810
- stripeSubscriptionId: workspace.stripeSubscriptionId,
811
- });
812
-
813
- if (result.success) {
814
- results.success++;
815
- results.totalCallsReported += result.callCount;
816
- } else {
817
- results.failed++;
818
- results.errors.push(
819
- `${workspace.email}: ${result.error || "Unknown error"}`
820
- );
821
- }
822
- } catch (error: any) {
823
- results.failed++;
824
- results.errors.push(`${workspace.email}: ${error.message || "Unknown error"}`);
825
- console.error(`[Cron] Error processing workspace ${workspace._id}:`, error);
826
- // Continue with next workspace
827
- }
828
- }
829
-
830
- console.log("[Cron] Daily usage reporting complete:", JSON.stringify(results));
831
-
832
- return results;
833
- },
834
- });