@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
@@ -1,607 +0,0 @@
1
- import Stripe from "stripe";
2
- import { httpAction } from "./_generated/server";
3
- import { api } from "./_generated/api";
4
-
5
- // Initialize Stripe (will use env var at runtime)
6
- function getStripe(): Stripe {
7
- const key = process.env.STRIPE_SECRET_KEY;
8
- if (!key) {
9
- throw new Error("STRIPE_SECRET_KEY not configured");
10
- }
11
- return new Stripe(key);
12
- }
13
-
14
- // CORS headers
15
- const corsHeaders = {
16
- "Access-Control-Allow-Origin": "*",
17
- "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
18
- "Access-Control-Allow-Headers": "Content-Type, Authorization, stripe-signature",
19
- };
20
-
21
- function jsonResponse(data: unknown, status = 200) {
22
- return new Response(JSON.stringify(data), {
23
- status,
24
- headers: { "Content-Type": "application/json", ...corsHeaders },
25
- });
26
- }
27
-
28
- /**
29
- * Create Stripe Checkout Session
30
- * POST /api/billing/checkout
31
- */
32
- export const createCheckoutSession = httpAction(async (ctx, request) => {
33
- try {
34
- const body = await request.json();
35
- const { workspaceId, returnUrl, mode = "setup" } = body;
36
-
37
- if (!workspaceId) {
38
- return jsonResponse({ error: "workspaceId required" }, 400);
39
- }
40
-
41
- const stripe = getStripe();
42
-
43
- // Get workspace
44
- const workspace = await ctx.runQuery(api.billing.getWorkspace, {
45
- id: workspaceId,
46
- });
47
-
48
- if (!workspace) {
49
- return jsonResponse({ error: "Workspace not found" }, 404);
50
- }
51
-
52
- let customerId = workspace.stripeCustomerId;
53
-
54
- // Create Stripe customer if doesn't exist
55
- if (!customerId) {
56
- const customer = await stripe.customers.create({
57
- email: workspace.email,
58
- metadata: {
59
- workspaceId: workspaceId,
60
- source: "apiclaw",
61
- },
62
- });
63
- customerId = customer.id;
64
-
65
- // Link customer to workspace
66
- await ctx.runMutation(api.billing.linkCustomer, {
67
- workspaceId: workspaceId,
68
- stripeCustomerId: customerId,
69
- });
70
- }
71
-
72
- const baseUrl = returnUrl || "https://apiclaw.cloud";
73
-
74
- // Create checkout session
75
- if (mode === "setup") {
76
- // Setup mode - just save card for future billing
77
- const session = await stripe.checkout.sessions.create({
78
- customer: customerId,
79
- mode: "setup",
80
- payment_method_types: ["card"],
81
- success_url: `${baseUrl}/billing?session_id={CHECKOUT_SESSION_ID}&success=true`,
82
- cancel_url: `${baseUrl}/billing?canceled=true`,
83
- metadata: {
84
- workspaceId: workspaceId,
85
- },
86
- });
87
-
88
- return jsonResponse({
89
- checkoutUrl: session.url,
90
- sessionId: session.id,
91
- });
92
- } else if (mode === "subscription") {
93
- // Create metered subscription
94
- const priceId = process.env.STRIPE_PRICE_ID_USAGE;
95
- if (!priceId) {
96
- return jsonResponse({ error: "Usage price not configured" }, 500);
97
- }
98
-
99
- const session = await stripe.checkout.sessions.create({
100
- customer: customerId,
101
- mode: "subscription",
102
- line_items: [
103
- {
104
- price: priceId,
105
- },
106
- ],
107
- success_url: `${baseUrl}/billing?session_id={CHECKOUT_SESSION_ID}&success=true`,
108
- cancel_url: `${baseUrl}/billing?canceled=true`,
109
- metadata: {
110
- workspaceId: workspaceId,
111
- },
112
- });
113
-
114
- return jsonResponse({
115
- checkoutUrl: session.url,
116
- sessionId: session.id,
117
- });
118
- } else {
119
- return jsonResponse({ error: "Invalid mode. Use 'setup' or 'subscription'" }, 400);
120
- }
121
- } catch (e: any) {
122
- console.error("Checkout error:", e);
123
- return jsonResponse({ error: e.message || "Failed to create checkout" }, 500);
124
- }
125
- });
126
-
127
- /**
128
- * Create Stripe Billing Portal Session
129
- * POST /api/billing/portal
130
- */
131
- export const createPortalSession = httpAction(async (ctx, request) => {
132
- try {
133
- const body = await request.json();
134
- const { workspaceId, returnUrl } = body;
135
-
136
- if (!workspaceId) {
137
- return jsonResponse({ error: "workspaceId required" }, 400);
138
- }
139
-
140
- const stripe = getStripe();
141
-
142
- // Get workspace
143
- const workspace = await ctx.runQuery(api.billing.getWorkspace, {
144
- id: workspaceId,
145
- });
146
-
147
- if (!workspace) {
148
- return jsonResponse({ error: "Workspace not found" }, 404);
149
- }
150
-
151
- if (!workspace.stripeCustomerId) {
152
- return jsonResponse(
153
- { error: "No billing account. Add a payment method first." },
154
- 400
155
- );
156
- }
157
-
158
- const session = await stripe.billingPortal.sessions.create({
159
- customer: workspace.stripeCustomerId,
160
- return_url: returnUrl || "https://apiclaw.com/workspace?tab=settings&portal=success",
161
- });
162
-
163
- return jsonResponse({
164
- portalUrl: session.url,
165
- });
166
- } catch (e: any) {
167
- console.error("Portal error:", e);
168
- return jsonResponse({ error: e.message || "Failed to create portal session" }, 500);
169
- }
170
- });
171
-
172
- /**
173
- * Stripe Webhook Handler
174
- * POST /api/webhooks/stripe
175
- */
176
- export const handleStripeWebhook = httpAction(async (ctx, request) => {
177
- const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
178
- if (!webhookSecret) {
179
- console.error("STRIPE_WEBHOOK_SECRET not configured");
180
- return jsonResponse({ error: "Webhook not configured" }, 500);
181
- }
182
-
183
- const stripe = getStripe();
184
- const signature = request.headers.get("stripe-signature");
185
-
186
- if (!signature) {
187
- return jsonResponse({ error: "Missing stripe-signature header" }, 400);
188
- }
189
-
190
- let event: Stripe.Event;
191
-
192
- try {
193
- const body = await request.text();
194
- event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
195
- } catch (err: any) {
196
- console.error("Webhook signature verification failed:", err.message);
197
- return jsonResponse({ error: `Webhook Error: ${err.message}` }, 400);
198
- }
199
-
200
- // Handle the event
201
- try {
202
- switch (event.type) {
203
- case "checkout.session.completed": {
204
- const session = event.data.object as Stripe.Checkout.Session;
205
- await handleCheckoutComplete(ctx, session);
206
- break;
207
- }
208
-
209
- case "customer.subscription.created":
210
- case "customer.subscription.updated": {
211
- const subscription = event.data.object as Stripe.Subscription;
212
- await handleSubscriptionUpdate(ctx, subscription);
213
- break;
214
- }
215
-
216
- case "customer.subscription.deleted": {
217
- const subscription = event.data.object as Stripe.Subscription;
218
- await handleSubscriptionCanceled(ctx, subscription);
219
- break;
220
- }
221
-
222
- case "invoice.paid": {
223
- const invoice = event.data.object as Stripe.Invoice;
224
- await handleInvoicePaid(ctx, invoice);
225
- break;
226
- }
227
-
228
- case "invoice.payment_failed": {
229
- const invoice = event.data.object as Stripe.Invoice;
230
- await handlePaymentFailed(ctx, invoice);
231
- break;
232
- }
233
-
234
- case "setup_intent.succeeded": {
235
- const setupIntent = event.data.object as Stripe.SetupIntent;
236
- await handleSetupSuccess(ctx, setupIntent);
237
- break;
238
- }
239
-
240
- case "payment_method.attached": {
241
- const paymentMethod = event.data.object as Stripe.PaymentMethod;
242
- await handlePaymentMethodAttached(ctx, paymentMethod);
243
- break;
244
- }
245
-
246
- case "payment_method.detached": {
247
- const paymentMethod = event.data.object as Stripe.PaymentMethod;
248
- await handlePaymentMethodDetached(ctx, paymentMethod);
249
- break;
250
- }
251
-
252
- default:
253
- console.log(`Unhandled event type: ${event.type}`);
254
- }
255
-
256
- return jsonResponse({ received: true, type: event.type });
257
- } catch (e: any) {
258
- console.error(`Error handling ${event.type}:`, e);
259
- // Return 200 to prevent Stripe retries for business logic errors
260
- return jsonResponse({ received: true, error: e.message });
261
- }
262
- });
263
-
264
- // ============================================
265
- // Webhook Event Handlers
266
- // ============================================
267
-
268
- async function handleCheckoutComplete(
269
- ctx: any,
270
- session: Stripe.Checkout.Session
271
- ) {
272
- const workspaceId = session.metadata?.workspaceId;
273
- if (!workspaceId) {
274
- console.log("No workspaceId in checkout session metadata");
275
- return;
276
- }
277
-
278
- // If subscription mode, the subscription webhook will handle it
279
- if (session.mode === "subscription" && session.subscription) {
280
- console.log("Subscription checkout completed, waiting for subscription webhook");
281
- return;
282
- }
283
-
284
- // If setup mode, upgrade to usage-based billing
285
- if (session.mode === "setup") {
286
- await ctx.runMutation(api.billing.updateSubscription, {
287
- workspaceId: workspaceId,
288
- billingPlan: "usage_based",
289
- });
290
- console.log(`Workspace ${workspaceId} upgraded to usage-based billing`);
291
- }
292
- }
293
-
294
- // Map Stripe product IDs to APIClaw billing plans
295
- const PRODUCT_PLAN_MAP: Record<string, string> = {
296
- "prod_UEPEBJUtrXDTLS": "pro", // APIClaw Pro ($79/mo)
297
- "prod_UEPFCcnUqfIaIn": "scale", // APIClaw Scale ($249/mo)
298
- };
299
-
300
- async function handleSubscriptionUpdate(
301
- ctx: any,
302
- subscription: Stripe.Subscription
303
- ) {
304
- const customerId =
305
- typeof subscription.customer === "string"
306
- ? subscription.customer
307
- : subscription.customer.id;
308
-
309
- // Get workspace by customer ID
310
- const workspace = await ctx.runQuery(api.billing.getByStripeCustomerId, {
311
- stripeCustomerId: customerId,
312
- });
313
-
314
- if (!workspace) {
315
- console.log(`No workspace found for customer ${customerId}`);
316
- return;
317
- }
318
-
319
- // Determine plan from subscription product
320
- let plan = "free";
321
- if (subscription.status === "active") {
322
- // Check each subscription item's product to determine the plan
323
- for (const item of subscription.items.data) {
324
- const productId = typeof item.price.product === "string"
325
- ? item.price.product
326
- : item.price.product.id;
327
- if (PRODUCT_PLAN_MAP[productId]) {
328
- plan = PRODUCT_PLAN_MAP[productId];
329
- break;
330
- }
331
- }
332
- // Fallback: if no known product matched but subscription is active
333
- if (plan === "free") {
334
- plan = "usage_based";
335
- }
336
- }
337
-
338
- await ctx.runMutation(api.billing.updateSubscription, {
339
- workspaceId: workspace._id,
340
- stripeSubscriptionId: subscription.id,
341
- billingPlan: plan,
342
- });
343
-
344
- console.log(`Subscription ${subscription.id} updated for workspace ${workspace._id} -> plan: ${plan}`);
345
- }
346
-
347
- async function handleSubscriptionCanceled(
348
- ctx: any,
349
- subscription: Stripe.Subscription
350
- ) {
351
- const customerId =
352
- typeof subscription.customer === "string"
353
- ? subscription.customer
354
- : subscription.customer.id;
355
-
356
- const workspace = await ctx.runQuery(api.billing.getByStripeCustomerId, {
357
- stripeCustomerId: customerId,
358
- });
359
-
360
- if (!workspace) {
361
- console.log(`No workspace found for customer ${customerId}`);
362
- return;
363
- }
364
-
365
- // Downgrade to free and reset usage
366
- await ctx.runMutation(api.billing.updateSubscription, {
367
- workspaceId: workspace._id,
368
- stripeSubscriptionId: undefined,
369
- billingPlan: "free",
370
- });
371
-
372
- // Reset usage count to 0 for clean slate on free tier
373
- await ctx.runMutation(api.billing.resetUsageOnCancellation, {
374
- workspaceId: workspace._id,
375
- });
376
-
377
- console.log(`Workspace ${workspace._id} downgraded to free (subscription canceled)`);
378
- }
379
-
380
- async function handleInvoicePaid(ctx: any, invoice: Stripe.Invoice) {
381
- const customerId =
382
- typeof invoice.customer === "string"
383
- ? invoice.customer
384
- : invoice.customer?.id;
385
-
386
- if (!customerId) return;
387
-
388
- const workspace = await ctx.runQuery(api.billing.getByStripeCustomerId, {
389
- stripeCustomerId: customerId,
390
- });
391
-
392
- if (!workspace) {
393
- console.log(`No workspace found for customer ${customerId}`);
394
- return;
395
- }
396
-
397
- // Calculate call count from line items (for metered billing)
398
- let callCount = 0;
399
- if (invoice.lines?.data) {
400
- for (const line of invoice.lines.data) {
401
- if (line.quantity) {
402
- callCount += line.quantity;
403
- }
404
- }
405
- }
406
-
407
- await ctx.runMutation(api.billing.processPayment, {
408
- stripeInvoiceId: invoice.id,
409
- workspaceId: workspace._id,
410
- amount: invoice.amount_paid,
411
- periodStart: invoice.period_start * 1000,
412
- periodEnd: invoice.period_end * 1000,
413
- callCount,
414
- pdfUrl: invoice.invoice_pdf || undefined,
415
- });
416
-
417
- console.log(`Invoice ${invoice.id} processed for workspace ${workspace._id}`);
418
- }
419
-
420
- async function handlePaymentFailed(ctx: any, invoice: Stripe.Invoice) {
421
- const customerId =
422
- typeof invoice.customer === "string"
423
- ? invoice.customer
424
- : invoice.customer?.id;
425
-
426
- if (!customerId) return;
427
-
428
- // Update invoice status to failed
429
- await ctx.runMutation(api.billing.updateInvoiceStatus, {
430
- stripeInvoiceId: invoice.id,
431
- status: "failed",
432
- });
433
-
434
- console.log(`Payment failed for invoice ${invoice.id}`);
435
-
436
- // TODO: Send notification to user about failed payment
437
- }
438
-
439
- async function handleSetupSuccess(
440
- ctx: any,
441
- setupIntent: Stripe.SetupIntent
442
- ) {
443
- // Card saved successfully
444
- const customerId =
445
- typeof setupIntent.customer === "string"
446
- ? setupIntent.customer
447
- : setupIntent.customer?.id;
448
-
449
- if (!customerId) return;
450
-
451
- const workspace = await ctx.runQuery(api.billing.getByStripeCustomerId, {
452
- stripeCustomerId: customerId,
453
- });
454
-
455
- if (!workspace) {
456
- console.log(`No workspace found for customer ${customerId}`);
457
- return;
458
- }
459
-
460
- // Only upgrade if on free tier (don't downgrade existing paid users)
461
- if (!workspace.billingPlan || workspace.billingPlan === "free") {
462
- const stripe = getStripe();
463
-
464
- // Set the payment method as default for invoices
465
- const paymentMethodId =
466
- typeof setupIntent.payment_method === "string"
467
- ? setupIntent.payment_method
468
- : setupIntent.payment_method?.id;
469
-
470
- if (paymentMethodId) {
471
- await stripe.customers.update(customerId, {
472
- invoice_settings: {
473
- default_payment_method: paymentMethodId,
474
- },
475
- });
476
- }
477
-
478
- // Check if customer already has an active metered subscription
479
- const METERED_PRICE_ID = process.env.STRIPE_PRICE_ID_USAGE || "price_1TL038RtJYK3aJTqODoFAiVT";
480
- const existingSubs = await stripe.subscriptions.list({
481
- customer: customerId,
482
- status: "active",
483
- limit: 10,
484
- });
485
-
486
- let subscriptionId: string | undefined;
487
- const hasMetered = existingSubs.data.some((sub) =>
488
- sub.items.data.some((item) => item.price.id === METERED_PRICE_ID)
489
- );
490
-
491
- if (!hasMetered) {
492
- // Create metered subscription so Stripe can invoice usage
493
- try {
494
- const subscription = await stripe.subscriptions.create({
495
- customer: customerId,
496
- items: [{ price: METERED_PRICE_ID }],
497
- payment_behavior: "default_incomplete",
498
- payment_settings: {
499
- save_default_payment_method: "on_subscription",
500
- },
501
- });
502
- subscriptionId = subscription.id;
503
- console.log(`Created metered subscription ${subscription.id} for customer ${customerId}`);
504
- } catch (subError: any) {
505
- console.error(`Failed to create metered subscription for ${customerId}:`, subError.message);
506
- // Still upgrade the tier even if subscription creation fails
507
- // User can retry later
508
- }
509
- } else {
510
- subscriptionId = existingSubs.data.find((sub) =>
511
- sub.items.data.some((item) => item.price.id === METERED_PRICE_ID)
512
- )?.id;
513
- console.log(`Customer ${customerId} already has metered subscription ${subscriptionId}`);
514
- }
515
-
516
- // Upgrade workspace to usage_based
517
- await ctx.runMutation(api.billing.updateSubscription, {
518
- workspaceId: workspace._id,
519
- stripeSubscriptionId: subscriptionId,
520
- billingPlan: "usage_based",
521
- });
522
-
523
- // Store payment method info
524
- if (paymentMethodId) {
525
- try {
526
- const pm = await stripe.paymentMethods.retrieve(paymentMethodId);
527
- if (pm.card) {
528
- await ctx.runMutation(api.billing.updatePaymentMethodInfo, {
529
- workspaceId: workspace._id,
530
- hasPaymentMethod: true,
531
- paymentMethodType: pm.type,
532
- cardBrand: pm.card.brand,
533
- cardLast4: pm.card.last4,
534
- });
535
- }
536
- } catch { /* non-critical */ }
537
- }
538
-
539
- console.log(`Workspace ${workspace._id} upgraded to usage_based with subscription ${subscriptionId}`);
540
- }
541
- }
542
-
543
- async function handlePaymentMethodAttached(
544
- ctx: any,
545
- paymentMethod: Stripe.PaymentMethod
546
- ) {
547
- // Payment method attached to customer
548
- const customerId =
549
- typeof paymentMethod.customer === "string"
550
- ? paymentMethod.customer
551
- : paymentMethod.customer?.id;
552
-
553
- if (!customerId) {
554
- console.log("Payment method attached but no customer ID");
555
- return;
556
- }
557
-
558
- const workspace = await ctx.runQuery(api.billing.getByStripeCustomerId, {
559
- stripeCustomerId: customerId,
560
- });
561
-
562
- if (!workspace) {
563
- console.log(`No workspace found for customer ${customerId}`);
564
- return;
565
- }
566
-
567
- // Sync payment method info
568
- await ctx.runMutation(api.billing.updatePaymentMethodInfo, {
569
- workspaceId: workspace._id,
570
- hasPaymentMethod: true,
571
- paymentMethodType: paymentMethod.type,
572
- cardBrand: paymentMethod.card?.brand,
573
- cardLast4: paymentMethod.card?.last4,
574
- });
575
-
576
- console.log(`Payment method attached for workspace ${workspace._id}`);
577
- }
578
-
579
- async function handlePaymentMethodDetached(
580
- ctx: any,
581
- paymentMethod: Stripe.PaymentMethod
582
- ) {
583
- // When a payment method is detached, we need to check if customer still has payment methods
584
- // Since customer info isn't available on detached event, we log it
585
- console.log(`Payment method ${paymentMethod.id} detached`);
586
-
587
- // Note: In a production system, you might want to:
588
- // 1. Query Stripe for remaining payment methods
589
- // 2. If no payment methods remain, downgrade the workspace
590
- // For now, we rely on subscription cancellation to handle downgrades
591
- }
592
-
593
- // ============================================
594
- // OPTIONS handlers for CORS
595
- // ============================================
596
-
597
- export const checkoutOptions = httpAction(async () => {
598
- return new Response(null, { headers: corsHeaders });
599
- });
600
-
601
- export const portalOptions = httpAction(async () => {
602
- return new Response(null, { headers: corsHeaders });
603
- });
604
-
605
- export const webhookOptions = httpAction(async () => {
606
- return new Response(null, { headers: corsHeaders });
607
- });