@nordsym/apiclaw 1.5.16 → 1.5.18

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 (172) hide show
  1. package/convex/http.js +196 -0
  2. package/convex/http.js.map +1 -1
  3. package/convex/http.ts +201 -0
  4. package/convex/http.ts.bak +934 -0
  5. package/dist/analytics.d.ts +0 -4
  6. package/dist/analytics.d.ts.map +1 -1
  7. package/dist/analytics.js +0 -1
  8. package/dist/analytics.js.map +1 -1
  9. package/dist/bin.js +1 -1
  10. package/dist/cli/commands/mcp-install.d.ts.map +1 -1
  11. package/dist/cli/commands/mcp-install.js +8 -87
  12. package/dist/cli/commands/mcp-install.js.map +1 -1
  13. package/dist/cli/index.js +0 -7
  14. package/dist/credentials.d.ts.map +1 -1
  15. package/dist/credentials.js +0 -128
  16. package/dist/credentials.js.map +1 -1
  17. package/dist/discovery.d.ts.map +1 -1
  18. package/dist/discovery.js +82 -191
  19. package/dist/discovery.js.map +1 -1
  20. package/dist/http-api.d.ts.map +1 -1
  21. package/dist/http-api.js +33 -17
  22. package/dist/http-api.js.map +1 -1
  23. package/dist/proxy.js +1 -1
  24. package/dist/proxy.js.map +1 -1
  25. package/landing/next-env.d.ts +0 -1
  26. package/landing/src/app/api/auth/magic-link/route.ts +1 -1
  27. package/landing/src/app/auth/verify/page.tsx +0 -6
  28. package/landing/src/app/dashboard/verify/page.tsx +0 -6
  29. package/landing/src/app/join/page.tsx +0 -6
  30. package/landing/src/app/layout.tsx +2 -2
  31. package/landing/src/app/login/page.tsx +1 -1
  32. package/landing/src/app/mou/[partnerId]/page.tsx +0 -6
  33. package/landing/src/app/page.tsx +18 -39
  34. package/landing/src/app/providers/dashboard/[apiId]/actions/[actionId]/edit/page.tsx +0 -6
  35. package/landing/src/app/providers/dashboard/[apiId]/actions/new/page.tsx +0 -5
  36. package/landing/src/app/providers/dashboard/[apiId]/actions/page.tsx +0 -5
  37. package/landing/src/app/providers/dashboard/[apiId]/direct-call/page.tsx +1 -6
  38. package/landing/src/app/providers/dashboard/[apiId]/page.tsx +0 -5
  39. package/landing/src/app/providers/dashboard/[apiId]/test/page.tsx +0 -5
  40. package/landing/src/app/providers/dashboard/layout.tsx +6 -6
  41. package/landing/src/app/providers/dashboard/login/page.tsx +1 -1
  42. package/landing/src/app/providers/dashboard/page.tsx +1 -1
  43. package/landing/src/app/providers/dashboard/verify/page.tsx +0 -6
  44. package/landing/src/app/providers/layout.tsx +1 -1
  45. package/landing/src/app/upgrade/page.tsx +0 -6
  46. package/landing/src/app/workspace/page.tsx +0 -6
  47. package/landing/src/components/HeroTabs.tsx +2 -2
  48. package/landing/src/components/{Workspace.tsx → ProviderDashboard.tsx} +2 -2
  49. package/landing/src/components/VideoDemo.tsx +10 -21
  50. package/landing/src/lib/mock-data.ts +1 -1
  51. package/landing/src/lib/stats.json +1 -1
  52. package/package.json +4 -6
  53. package/src/analytics.ts +0 -5
  54. package/src/bin.ts +1 -1
  55. package/src/cli/commands/mcp-install.ts +8 -90
  56. package/src/cli/index.ts +0 -8
  57. package/src/credentials.ts +0 -136
  58. package/src/discovery.ts +82 -191
  59. package/src/http-api.ts +34 -18
  60. package/src/proxy.ts +1 -1
  61. package/APILAYER_STATUS_2026-03-24.md +0 -38
  62. package/CHANGELOG-WHITELIST-V2.md +0 -269
  63. package/HIVR-WHITELIST-STATUS.md +0 -205
  64. package/HIVR-WHITELIST.md +0 -148
  65. package/TERMINOLOGY-AUDIT.md +0 -99
  66. package/TERMINOLOGY-FIXED.md +0 -74
  67. package/VIDEO-DEMO-GUIDE.md +0 -82
  68. package/WHITELIST-ARCHITECTURE.md +0 -379
  69. package/api/discover.ts +0 -71
  70. package/api/health.ts +0 -20
  71. package/direct-test.mjs +0 -51
  72. package/dist/access-control.d.ts +0 -45
  73. package/dist/access-control.d.ts.map +0 -1
  74. package/dist/access-control.js +0 -142
  75. package/dist/access-control.js.map +0 -1
  76. package/dist/chain-types.d.ts +0 -187
  77. package/dist/chain-types.d.ts.map +0 -1
  78. package/dist/chain-types.js +0 -33
  79. package/dist/chain-types.js.map +0 -1
  80. package/dist/convex/adminActivate.js +0 -46
  81. package/dist/convex/adminStats.js +0 -41
  82. package/dist/convex/agents.js +0 -498
  83. package/dist/convex/analytics.js +0 -165
  84. package/dist/convex/billing.js +0 -654
  85. package/dist/convex/capabilities.js +0 -144
  86. package/dist/convex/chains.js +0 -1041
  87. package/dist/convex/credits.js +0 -185
  88. package/dist/convex/crons.js +0 -16
  89. package/dist/convex/directCall.js +0 -626
  90. package/dist/convex/earnProgress.js +0 -648
  91. package/dist/convex/email.js +0 -299
  92. package/dist/convex/feedback.js +0 -226
  93. package/dist/convex/http.js +0 -909
  94. package/dist/convex/logs.js +0 -486
  95. package/dist/convex/mou.js +0 -81
  96. package/dist/convex/providerKeys.js +0 -256
  97. package/dist/convex/providers.js +0 -755
  98. package/dist/convex/purchases.js +0 -156
  99. package/dist/convex/ratelimit.js +0 -90
  100. package/dist/convex/schema.js +0 -709
  101. package/dist/convex/searchLogs.js +0 -128
  102. package/dist/convex/spendAlerts.js +0 -379
  103. package/dist/convex/stripeActions.js +0 -410
  104. package/dist/convex/teams.js +0 -214
  105. package/dist/convex/telemetry.js +0 -73
  106. package/dist/convex/usage.js +0 -228
  107. package/dist/convex/waitlist.js +0 -48
  108. package/dist/convex/webhooks.js +0 -409
  109. package/dist/convex/workspaces.js +0 -879
  110. package/dist/hivr-whitelist.d.ts +0 -18
  111. package/dist/hivr-whitelist.d.ts.map +0 -1
  112. package/dist/hivr-whitelist.js +0 -95
  113. package/dist/hivr-whitelist.js.map +0 -1
  114. package/dist/http-server-minimal.d.ts +0 -7
  115. package/dist/http-server-minimal.d.ts.map +0 -1
  116. package/dist/http-server-minimal.js +0 -126
  117. package/dist/http-server-minimal.js.map +0 -1
  118. package/dist/product-whitelist.d.ts +0 -37
  119. package/dist/product-whitelist.d.ts.map +0 -1
  120. package/dist/product-whitelist.js +0 -203
  121. package/dist/product-whitelist.js.map +0 -1
  122. package/dist/src/analytics.js +0 -129
  123. package/dist/src/bin.js +0 -17
  124. package/dist/src/capability-router.js +0 -240
  125. package/dist/src/chainExecutor.js +0 -451
  126. package/dist/src/chainResolver.js +0 -518
  127. package/dist/src/cli/commands/doctor.js +0 -324
  128. package/dist/src/cli/commands/mcp-install.js +0 -255
  129. package/dist/src/cli/commands/restore.js +0 -259
  130. package/dist/src/cli/commands/setup.js +0 -205
  131. package/dist/src/cli/commands/uninstall.js +0 -188
  132. package/dist/src/cli/index.js +0 -111
  133. package/dist/src/cli.js +0 -302
  134. package/dist/src/confirmation.js +0 -240
  135. package/dist/src/credentials.js +0 -357
  136. package/dist/src/credits.js +0 -260
  137. package/dist/src/crypto.js +0 -66
  138. package/dist/src/discovery.js +0 -504
  139. package/dist/src/enterprise/env.js +0 -123
  140. package/dist/src/enterprise/script-generator.js +0 -460
  141. package/dist/src/execute-dynamic.js +0 -473
  142. package/dist/src/execute.js +0 -1727
  143. package/dist/src/index.js +0 -2062
  144. package/dist/src/metered.js +0 -80
  145. package/dist/src/open-apis.js +0 -276
  146. package/dist/src/proxy.js +0 -28
  147. package/dist/src/session.js +0 -86
  148. package/dist/src/stripe.js +0 -407
  149. package/dist/src/telemetry.js +0 -49
  150. package/dist/src/types.js +0 -2
  151. package/dist/src/utils/backup.js +0 -181
  152. package/dist/src/utils/config.js +0 -220
  153. package/dist/src/utils/os.js +0 -105
  154. package/dist/src/utils/paths.js +0 -159
  155. package/landing/pages/api/discover.ts +0 -43
  156. package/landing/pages/api/health.ts +0 -20
  157. package/scripts/test-whitelist-v2.sh +0 -128
  158. package/src/access-control.ts +0 -174
  159. package/src/hivr-whitelist.ts +0 -110
  160. package/src/http-server-minimal.ts +0 -154
  161. package/src/product-whitelist.ts +0 -246
  162. package/test-actual-handlers.ts +0 -92
  163. package/test-apilayer-all-14.ts +0 -249
  164. package/test-apilayer-fixed.ts +0 -248
  165. package/test-direct-endpoints.ts +0 -174
  166. package/test-exact-endpoints.ts +0 -144
  167. package/test-final.ts +0 -83
  168. package/test-full-routing.ts +0 -100
  169. package/test-handlers-correct.ts +0 -217
  170. package/test-numverify-key.ts +0 -41
  171. package/test-via-handlers.ts +0 -92
  172. package/test-worldnews.mjs +0 -26
@@ -0,0 +1,934 @@
1
+ import { httpRouter } from "convex/server";
2
+ import { httpAction } from "./_generated/server";
3
+ import { api, internal } from "./_generated/api";
4
+ import {
5
+ createCheckoutSession,
6
+ createPortalSession,
7
+ handleStripeWebhook,
8
+ checkoutOptions,
9
+ portalOptions,
10
+ webhookOptions,
11
+ } from "./stripeActions";
12
+
13
+ const http = httpRouter();
14
+
15
+ // Provider catalog
16
+ const PROVIDERS = {
17
+ "46elks": {
18
+ name: "46elks",
19
+ description: "SMS API for EU/Nordics. GDPR compliant.",
20
+ category: "sms",
21
+ pricing: "~$0.035/SMS",
22
+ regions: ["EU", "Nordic"],
23
+ tags: ["sms", "eu", "gdpr", "nordic"],
24
+ },
25
+ twilio: {
26
+ name: "Twilio",
27
+ description: "SMS and Voice API. Global coverage.",
28
+ category: "sms",
29
+ pricing: "~$0.04/SMS, ~$0.01/min voice",
30
+ regions: ["Global"],
31
+ tags: ["sms", "voice", "global"],
32
+ },
33
+ resend: {
34
+ name: "Resend",
35
+ description: "Modern email API. Developer-friendly.",
36
+ category: "email",
37
+ pricing: "~$0.001/email",
38
+ regions: ["Global"],
39
+ tags: ["email", "transactional"],
40
+ },
41
+ brave_search: {
42
+ name: "Brave Search",
43
+ description: "Privacy-focused web search API.",
44
+ category: "search",
45
+ pricing: "~$0.005/search",
46
+ regions: ["Global"],
47
+ tags: ["search", "web", "privacy"],
48
+ },
49
+ openrouter: {
50
+ name: "OpenRouter",
51
+ description: "Multi-model LLM API. Access GPT, Claude, Llama, etc.",
52
+ category: "llm",
53
+ pricing: "Varies by model",
54
+ regions: ["Global"],
55
+ tags: ["llm", "ai", "gpt", "claude"],
56
+ },
57
+ elevenlabs: {
58
+ name: "ElevenLabs",
59
+ description: "Text-to-speech API. High quality voices.",
60
+ category: "tts",
61
+ pricing: "~$0.0003/char",
62
+ regions: ["Global"],
63
+ tags: ["tts", "voice", "audio"],
64
+ },
65
+ replicate: {
66
+ name: "Replicate",
67
+ description: "Run AI models (Whisper, SDXL, Llama, etc). Pay per prediction.",
68
+ category: "ai",
69
+ pricing: "Varies by model",
70
+ regions: ["Global"],
71
+ tags: ["ai", "ml", "whisper", "image", "audio", "transcription"],
72
+ },
73
+ firecrawl: {
74
+ name: "Firecrawl",
75
+ description: "Web scraping and crawling API. Extract clean data from any URL.",
76
+ category: "scraping",
77
+ pricing: "~$0.001/page",
78
+ regions: ["Global"],
79
+ tags: ["scraping", "web", "crawl", "extract"],
80
+ },
81
+ github: {
82
+ name: "GitHub",
83
+ description: "GitHub API. Search repos, manage code, access developer data.",
84
+ category: "code",
85
+ pricing: "Free tier available",
86
+ regions: ["Global"],
87
+ tags: ["github", "code", "repos", "developer"],
88
+ },
89
+ e2b: {
90
+ name: "E2B",
91
+ description: "Secure code sandbox for AI agents. Run Python, shell commands in isolated environments.",
92
+ category: "sandbox",
93
+ pricing: "$0.000028/s (2 vCPU)",
94
+ regions: ["Global"],
95
+ tags: ["sandbox", "code", "python", "execution", "ai", "agents"],
96
+ },
97
+ } as const;
98
+
99
+ // CORS headers
100
+ const corsHeaders = {
101
+ "Access-Control-Allow-Origin": "*",
102
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
103
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
104
+ };
105
+
106
+ // Helper for JSON responses
107
+ function jsonResponse(data: unknown, status = 200) {
108
+ return new Response(JSON.stringify(data), {
109
+ status,
110
+ headers: { "Content-Type": "application/json", ...corsHeaders },
111
+ });
112
+ }
113
+
114
+ // Helper to validate session and log API usage
115
+ async function validateAndLogProxyCall(
116
+ ctx: any,
117
+ request: Request,
118
+ provider: string,
119
+ action: string
120
+ ): Promise<{ valid: boolean; workspaceId?: string; subagentId?: string; error?: string }> {
121
+ const sessionToken = request.headers.get("X-APIClaw-Session");
122
+ const subagentId = request.headers.get("X-APIClaw-Subagent") || "unknown";
123
+
124
+ if (!sessionToken) {
125
+ // Allow calls without session but don't log to workspace
126
+ return { valid: true, subagentId };
127
+ }
128
+
129
+ try {
130
+ // Validate session
131
+ const session = await ctx.runQuery(api.workspaces.getSession, { token: sessionToken });
132
+
133
+ if (!session) {
134
+ // Allow call anyway but log warning
135
+ console.warn("[Proxy] Invalid session token, allowing call but not logging");
136
+ return { valid: true, subagentId };
137
+ }
138
+
139
+ // Log the API call
140
+ await ctx.runMutation(api.logs.createProxyLog, {
141
+ workspaceId: session.workspaceId,
142
+ provider,
143
+ action,
144
+ subagentId,
145
+ sessionToken,
146
+ });
147
+
148
+ // Increment usage
149
+ await ctx.runMutation(api.workspaces.incrementUsage, {
150
+ workspaceId: session.workspaceId,
151
+ });
152
+
153
+ return { valid: true, workspaceId: session.workspaceId, subagentId };
154
+ } catch (e: any) {
155
+ console.error("[Proxy] Session validation error:", e);
156
+ // Allow call but don't log on error
157
+ return { valid: true, subagentId };
158
+ }
159
+ }
160
+
161
+ // OPTIONS handler for CORS
162
+ http.route({
163
+ path: "/api/discover",
164
+ method: "OPTIONS",
165
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
166
+ });
167
+
168
+ http.route({
169
+ path: "/api/details",
170
+ method: "OPTIONS",
171
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
172
+ });
173
+
174
+ http.route({
175
+ path: "/api/balance",
176
+ method: "OPTIONS",
177
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
178
+ });
179
+
180
+ http.route({
181
+ path: "/api/purchase",
182
+ method: "OPTIONS",
183
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
184
+ });
185
+
186
+ http.route({
187
+ path: "/admin/grant-credits",
188
+ method: "OPTIONS",
189
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
190
+ });
191
+
192
+ // Discover APIs
193
+ http.route({
194
+ path: "/api/discover",
195
+ method: "POST",
196
+ handler: httpAction(async (ctx, request) => {
197
+ try {
198
+ const startTime = Date.now();
199
+ const body = await request.json();
200
+ const query = (body.query || "").toLowerCase();
201
+
202
+ // Get optional auth context
203
+ const sessionToken = request.headers.get("X-APIClaw-Session");
204
+ const userAgent = request.headers.get("User-Agent");
205
+
206
+ const results = Object.entries(PROVIDERS)
207
+ .filter(([id, provider]) => {
208
+ if (!query) return true;
209
+ return (
210
+ provider.name.toLowerCase().includes(query) ||
211
+ provider.description.toLowerCase().includes(query) ||
212
+ provider.category.toLowerCase().includes(query) ||
213
+ provider.tags.some((tag) => tag.includes(query))
214
+ );
215
+ })
216
+ .map(([id, provider]) => ({
217
+ providerId: id,
218
+ ...provider,
219
+ }));
220
+
221
+ const responseTimeMs = Date.now() - startTime;
222
+
223
+ // Log the search (fire and forget)
224
+ if (query) {
225
+ ctx.runMutation(internal.searchLogs.logSearch, {
226
+ query: body.query || "", // Original query (not lowercased)
227
+ resultsCount: results.length,
228
+ matchedProviders: results.map(r => r.providerId),
229
+ sessionToken: sessionToken || undefined,
230
+ userAgent: userAgent || undefined,
231
+ responseTimeMs,
232
+ }).catch(() => {}); // Ignore errors, don't block response
233
+ }
234
+
235
+ return jsonResponse({ providers: results, total: results.length });
236
+ } catch (e) {
237
+ return jsonResponse({ error: "Invalid request" }, 400);
238
+ }
239
+ }),
240
+ });
241
+
242
+ // Get provider details
243
+ http.route({
244
+ path: "/api/details",
245
+ method: "POST",
246
+ handler: httpAction(async (ctx, request) => {
247
+ try {
248
+ const body = await request.json();
249
+ const { providerId } = body;
250
+
251
+ if (!providerId) {
252
+ return jsonResponse({ error: "providerId required" }, 400);
253
+ }
254
+
255
+ const provider = PROVIDERS[providerId as keyof typeof PROVIDERS];
256
+ if (!provider) {
257
+ return jsonResponse({ error: "Provider not found" }, 404);
258
+ }
259
+
260
+ return jsonResponse({
261
+ providerId,
262
+ ...provider,
263
+ creditsPerDollar: getCreditsPerDollar(providerId),
264
+ documentation: `https://apiclaw.com/docs/${providerId}`,
265
+ });
266
+ } catch (e) {
267
+ return jsonResponse({ error: "Invalid request" }, 400);
268
+ }
269
+ }),
270
+ });
271
+
272
+ // Check balance
273
+ http.route({
274
+ path: "/api/balance",
275
+ method: "GET",
276
+ handler: httpAction(async (ctx, request) => {
277
+ const url = new URL(request.url);
278
+ const agentId = url.searchParams.get("agentId");
279
+
280
+ if (!agentId) {
281
+ return jsonResponse({ error: "agentId required" }, 400);
282
+ }
283
+
284
+ const credits = await ctx.runQuery(api.credits.getAgentCredits, { agentId });
285
+
286
+ if (!credits) {
287
+ return jsonResponse({
288
+ agentId,
289
+ balanceUsd: 0,
290
+ currency: "USD",
291
+ message: "No account found. Top up to get started!",
292
+ });
293
+ }
294
+
295
+ return jsonResponse({
296
+ agentId: credits.agentId,
297
+ balanceUsd: credits.balanceUsd,
298
+ currency: credits.currency,
299
+ });
300
+ }),
301
+ });
302
+
303
+ // Purchase API access
304
+ http.route({
305
+ path: "/api/purchase",
306
+ method: "POST",
307
+ handler: httpAction(async (ctx, request) => {
308
+ try {
309
+ const body = await request.json();
310
+ const { agentId, providerId, amountUsd } = body;
311
+
312
+ if (!agentId || !providerId || !amountUsd) {
313
+ return jsonResponse(
314
+ { error: "agentId, providerId, and amountUsd required" },
315
+ 400
316
+ );
317
+ }
318
+
319
+ if (amountUsd < 1 || amountUsd > 1000) {
320
+ return jsonResponse(
321
+ { error: "amountUsd must be between 1 and 1000" },
322
+ 400
323
+ );
324
+ }
325
+
326
+ const provider = PROVIDERS[providerId as keyof typeof PROVIDERS];
327
+ if (!provider) {
328
+ return jsonResponse({ error: "Provider not found" }, 404);
329
+ }
330
+
331
+ // Check balance first
332
+ const credits = await ctx.runQuery(api.credits.getAgentCredits, { agentId });
333
+ if (!credits || credits.balanceUsd < amountUsd) {
334
+ return jsonResponse(
335
+ {
336
+ error: "Insufficient balance",
337
+ currentBalance: credits?.balanceUsd || 0,
338
+ required: amountUsd,
339
+ },
340
+ 402
341
+ );
342
+ }
343
+
344
+ // Execute purchase
345
+ const purchase = await ctx.runMutation(api.purchases.purchaseAccess, {
346
+ agentId,
347
+ providerId,
348
+ amountUsd,
349
+ credentials: generateCredentials(providerId),
350
+ });
351
+
352
+ if (!purchase) {
353
+ return jsonResponse({ error: "Purchase failed" }, 500);
354
+ }
355
+
356
+ return jsonResponse({
357
+ success: true,
358
+ purchase: {
359
+ id: purchase._id,
360
+ providerId: purchase.providerId,
361
+ amountUsd: purchase.amountUsd,
362
+ creditsGranted: purchase.creditsGranted,
363
+ status: purchase.status,
364
+ },
365
+ message: `Successfully purchased $${amountUsd} of ${provider.name} credits`,
366
+ });
367
+ } catch (e: any) {
368
+ return jsonResponse({ error: e.message || "Purchase failed" }, 400);
369
+ }
370
+ }),
371
+ });
372
+
373
+ // Admin: Grant credits
374
+ http.route({
375
+ path: "/admin/grant-credits",
376
+ method: "POST",
377
+ handler: httpAction(async (ctx, request) => {
378
+ try {
379
+ const body = await request.json();
380
+ const { agentId, amount, reason } = body;
381
+
382
+ if (!agentId || !amount) {
383
+ return jsonResponse({ error: "agentId and amount required" }, 400);
384
+ }
385
+
386
+ // TODO: Add admin auth check here
387
+ // For now, allow grants (this is for Hivr integration)
388
+
389
+ const result = await ctx.runMutation(api.credits.addCredits, {
390
+ agentId,
391
+ amountUsd: amount,
392
+ source: reason || "admin_grant",
393
+ });
394
+
395
+ return jsonResponse({
396
+ success: true,
397
+ agentId,
398
+ credited: amount,
399
+ newBalance: result?.balanceUsd,
400
+ reason,
401
+ });
402
+ } catch (e: any) {
403
+ return jsonResponse({ error: e.message || "Grant failed" }, 400);
404
+ }
405
+ }),
406
+ });
407
+
408
+ // Helper functions
409
+ function getCreditsPerDollar(providerId: string): number {
410
+ const rates: Record<string, number> = {
411
+ "46elks": 30,
412
+ twilio: 25,
413
+ resend: 1000,
414
+ brave_search: 200,
415
+ openrouter: 100,
416
+ elevenlabs: 3333,
417
+ };
418
+ return rates[providerId] || 100;
419
+ }
420
+
421
+ function generateCredentials(providerId: string): object {
422
+ // In production, this would generate or retrieve actual API keys
423
+ // For now, return placeholder indicating how to use
424
+ return {
425
+ type: "apiclaw_proxy",
426
+ endpoint: `https://brilliant-puffin-712.convex.site/proxy/${providerId}`,
427
+ note: "Use APIClaw proxy endpoint. Credentials managed automatically.",
428
+ };
429
+ }
430
+
431
+ export default http;
432
+
433
+ // ==============================================
434
+ // DIRECT CALL PROXY ENDPOINTS
435
+ // ==============================================
436
+
437
+ // OpenRouter proxy
438
+ http.route({
439
+ path: "/proxy/openrouter",
440
+ method: "POST",
441
+ handler: httpAction(async (ctx, request) => {
442
+ // Validate session and log usage
443
+ await validateAndLogProxyCall(ctx, request, "openrouter", "chat");
444
+
445
+ const OPENROUTER_KEY = process.env.OPENROUTER_API_KEY;
446
+ if (!OPENROUTER_KEY) {
447
+ return jsonResponse({ error: "OpenRouter not configured" }, 500);
448
+ }
449
+
450
+ try {
451
+ const body = await request.json();
452
+
453
+ const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
454
+ method: "POST",
455
+ headers: {
456
+ "Authorization": `Bearer ${OPENROUTER_KEY}`,
457
+ "Content-Type": "application/json",
458
+ "HTTP-Referer": "https://apiclaw.nordsym.com",
459
+ "X-Title": "APIClaw",
460
+ },
461
+ body: JSON.stringify(body),
462
+ });
463
+
464
+ const data = await response.json();
465
+ return jsonResponse(data, response.status);
466
+ } catch (e: any) {
467
+ return jsonResponse({ error: e.message }, 500);
468
+ }
469
+ }),
470
+ });
471
+
472
+ // Brave Search proxy
473
+ http.route({
474
+ path: "/proxy/brave_search",
475
+ method: "POST",
476
+ handler: httpAction(async (ctx, request) => {
477
+ // Validate session and log usage
478
+ await validateAndLogProxyCall(ctx, request, "brave_search", "search");
479
+
480
+ const BRAVE_KEY = process.env.BRAVE_API_KEY;
481
+ if (!BRAVE_KEY) {
482
+ return jsonResponse({ error: "Brave Search not configured" }, 500);
483
+ }
484
+
485
+ try {
486
+ const body = await request.json();
487
+ const { query, count = 10 } = body;
488
+
489
+ const url = new URL("https://api.search.brave.com/res/v1/web/search");
490
+ url.searchParams.set("q", query);
491
+ url.searchParams.set("count", String(count));
492
+
493
+ const response = await fetch(url.toString(), {
494
+ headers: { "X-Subscription-Token": BRAVE_KEY },
495
+ });
496
+
497
+ const data = await response.json();
498
+ return jsonResponse(data, response.status);
499
+ } catch (e: any) {
500
+ return jsonResponse({ error: e.message }, 500);
501
+ }
502
+ }),
503
+ });
504
+
505
+ // Resend email proxy
506
+ http.route({
507
+ path: "/proxy/resend",
508
+ method: "POST",
509
+ handler: httpAction(async (ctx, request) => {
510
+ // Validate session and log usage
511
+ await validateAndLogProxyCall(ctx, request, "resend", "send_email");
512
+
513
+ const RESEND_KEY = process.env.RESEND_API_KEY;
514
+ if (!RESEND_KEY) {
515
+ return jsonResponse({ error: "Resend not configured" }, 500);
516
+ }
517
+
518
+ try {
519
+ const body = await request.json();
520
+
521
+ const response = await fetch("https://api.resend.com/emails", {
522
+ method: "POST",
523
+ headers: {
524
+ "Authorization": `Bearer ${RESEND_KEY}`,
525
+ "Content-Type": "application/json",
526
+ },
527
+ body: JSON.stringify(body),
528
+ });
529
+
530
+ const data = await response.json();
531
+ return jsonResponse(data, response.status);
532
+ } catch (e: any) {
533
+ return jsonResponse({ error: e.message }, 500);
534
+ }
535
+ }),
536
+ });
537
+
538
+ // ElevenLabs TTS proxy
539
+ http.route({
540
+ path: "/proxy/elevenlabs",
541
+ method: "POST",
542
+ handler: httpAction(async (ctx, request) => {
543
+ // Validate session and log usage
544
+ await validateAndLogProxyCall(ctx, request, "elevenlabs", "text_to_speech");
545
+
546
+ const ELEVENLABS_KEY = process.env.ELEVENLABS_API_KEY;
547
+ if (!ELEVENLABS_KEY) {
548
+ return jsonResponse({ error: "ElevenLabs not configured" }, 500);
549
+ }
550
+
551
+ try {
552
+ const body = await request.json();
553
+ const { text, voice_id = "21m00Tcm4TlvDq8ikWAM" } = body;
554
+
555
+ const response = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voice_id}`, {
556
+ method: "POST",
557
+ headers: {
558
+ "xi-api-key": ELEVENLABS_KEY,
559
+ "Content-Type": "application/json",
560
+ },
561
+ body: JSON.stringify({
562
+ text,
563
+ model_id: "eleven_turbo_v2",
564
+ }),
565
+ });
566
+
567
+ if (!response.ok) {
568
+ const error = await response.text();
569
+ return jsonResponse({ error }, response.status);
570
+ }
571
+
572
+ // Return audio as base64
573
+ const arrayBuffer = await response.arrayBuffer();
574
+ const base64 = Buffer.from(arrayBuffer).toString("base64");
575
+
576
+ return jsonResponse({
577
+ audio_base64: base64,
578
+ content_type: "audio/mpeg",
579
+ });
580
+ } catch (e: any) {
581
+ return jsonResponse({ error: e.message }, 500);
582
+ }
583
+ }),
584
+ });
585
+
586
+ http.route({
587
+ path: "/proxy/openrouter",
588
+ method: "OPTIONS",
589
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
590
+ });
591
+
592
+ http.route({
593
+ path: "/proxy/brave_search",
594
+ method: "OPTIONS",
595
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
596
+ });
597
+
598
+ http.route({
599
+ path: "/proxy/resend",
600
+ method: "OPTIONS",
601
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
602
+ });
603
+
604
+ http.route({
605
+ path: "/proxy/elevenlabs",
606
+ method: "OPTIONS",
607
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
608
+ });
609
+
610
+ // 46elks SMS proxy
611
+ http.route({
612
+ path: "/proxy/46elks",
613
+ method: "POST",
614
+ handler: httpAction(async (ctx, request) => {
615
+ // Validate session and log usage
616
+ await validateAndLogProxyCall(ctx, request, "46elks", "send_sms");
617
+
618
+ const ELKS_USER = process.env.ELKS_API_USER;
619
+ const ELKS_PASS = process.env.ELKS_API_PASSWORD;
620
+ if (!ELKS_USER || !ELKS_PASS) {
621
+ return jsonResponse({ error: "46elks not configured" }, 500);
622
+ }
623
+
624
+ try {
625
+ const body = await request.json();
626
+ const { to, message, from = "APIClaw" } = body;
627
+
628
+ const auth = btoa(`${ELKS_USER}:${ELKS_PASS}`);
629
+
630
+ const response = await fetch("https://api.46elks.com/a1/sms", {
631
+ method: "POST",
632
+ headers: {
633
+ "Authorization": `Basic ${auth}`,
634
+ "Content-Type": "application/x-www-form-urlencoded",
635
+ },
636
+ body: new URLSearchParams({ from, to, message }),
637
+ });
638
+
639
+ const data = await response.json();
640
+ return jsonResponse(data, response.status);
641
+ } catch (e: any) {
642
+ return jsonResponse({ error: e.message }, 500);
643
+ }
644
+ }),
645
+ });
646
+
647
+ // Twilio SMS proxy
648
+ http.route({
649
+ path: "/proxy/twilio",
650
+ method: "POST",
651
+ handler: httpAction(async (ctx, request) => {
652
+ // Validate session and log usage
653
+ await validateAndLogProxyCall(ctx, request, "twilio", "send_sms");
654
+
655
+ const TWILIO_SID = process.env.TWILIO_ACCOUNT_SID;
656
+ const TWILIO_TOKEN = process.env.TWILIO_AUTH_TOKEN;
657
+ if (!TWILIO_SID || !TWILIO_TOKEN) {
658
+ return jsonResponse({ error: "Twilio not configured" }, 500);
659
+ }
660
+
661
+ try {
662
+ const body = await request.json();
663
+ const { to, message, from } = body;
664
+
665
+ if (!from) {
666
+ return jsonResponse({ error: "Twilio requires 'from' number" }, 400);
667
+ }
668
+
669
+ const auth = btoa(`${TWILIO_SID}:${TWILIO_TOKEN}`);
670
+
671
+ const response = await fetch(
672
+ `https://api.twilio.com/2010-04-01/Accounts/${TWILIO_SID}/Messages.json`,
673
+ {
674
+ method: "POST",
675
+ headers: {
676
+ "Authorization": `Basic ${auth}`,
677
+ "Content-Type": "application/x-www-form-urlencoded",
678
+ },
679
+ body: new URLSearchParams({ To: to, From: from, Body: message }),
680
+ }
681
+ );
682
+
683
+ const data = await response.json();
684
+ return jsonResponse(data, response.status);
685
+ } catch (e: any) {
686
+ return jsonResponse({ error: e.message }, 500);
687
+ }
688
+ }),
689
+ });
690
+
691
+ // CORS for new endpoints
692
+ http.route({
693
+ path: "/proxy/46elks",
694
+ method: "OPTIONS",
695
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
696
+ });
697
+
698
+ http.route({
699
+ path: "/proxy/twilio",
700
+ method: "OPTIONS",
701
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
702
+ });
703
+
704
+ // ==============================================
705
+ // WORKSPACE / MAGIC LINK ENDPOINTS
706
+ // ==============================================
707
+
708
+ // Create magic link and send email
709
+ http.route({
710
+ path: "/workspace/magic-link",
711
+ method: "POST",
712
+ handler: httpAction(async (ctx, request) => {
713
+ try {
714
+ const body = await request.json();
715
+ const { email, fingerprint } = body;
716
+
717
+ if (!email || !email.includes("@")) {
718
+ return jsonResponse({ error: "Valid email required" }, 400);
719
+ }
720
+
721
+ // Create magic link
722
+ const result = await ctx.runMutation(api.workspaces.createMagicLink, {
723
+ email: email.toLowerCase(),
724
+ fingerprint,
725
+ });
726
+
727
+ // Send email directly - SIMPLE HTML (complex tables get stripped by Gmail)
728
+ const verifyUrl = `https://apiclaw.nordsym.com/auth/verify?token=${result.token}`;
729
+ const html = `<div style="font-family:Arial,sans-serif;max-width:500px;margin:0 auto;padding:20px;">
730
+ <h1>🦞 APIClaw</h1>
731
+ <h2>An AI Agent Wants to Connect</h2>
732
+ <p>Click below to verify your email and activate your workspace.</p>
733
+ <p><a href="${verifyUrl}" style="background:#ef4444;color:white;padding:14px 32px;border-radius:8px;text-decoration:none;display:inline-block;">Verify Email</a></p>
734
+ <p style="color:#666;font-size:13px;">Free tier: 50 API calls. This link expires in 1 hour.</p>
735
+ <p style="color:#999;font-size:11px;">Or copy this link: ${verifyUrl}</p>
736
+ </div>`;
737
+
738
+ const RESEND_KEY = process.env.RESEND_API_KEY;
739
+ if (!RESEND_KEY) {
740
+ console.error("RESEND_API_KEY not configured");
741
+ return jsonResponse({ error: "Email service not configured" }, 500);
742
+ }
743
+
744
+ const emailResponse = await fetch("https://api.resend.com/emails", {
745
+ method: "POST",
746
+ headers: {
747
+ "Authorization": `Bearer ${RESEND_KEY}`,
748
+ "Content-Type": "application/json",
749
+ },
750
+ body: JSON.stringify({
751
+ from: "APIClaw <noreply@apiclaw.nordsym.com>",
752
+ to: email.toLowerCase(),
753
+ subject: "🦞 Verify Your Email — APIClaw",
754
+ html: html,
755
+ }),
756
+ });
757
+
758
+ if (!emailResponse.ok) {
759
+ const errorText = await emailResponse.text();
760
+ console.error("Resend error:", emailResponse.status, errorText);
761
+ return jsonResponse({ error: "Failed to send email", details: errorText }, 500);
762
+ }
763
+
764
+ const emailResult = await emailResponse.json();
765
+ console.log("Email sent successfully:", emailResult.id);
766
+
767
+ return jsonResponse({
768
+ success: true,
769
+ token: result.token,
770
+ expiresAt: result.expiresAt,
771
+ message: "Magic link sent! Check your email.",
772
+ emailId: emailResult.id,
773
+ });
774
+ } catch (e: any) {
775
+ console.error("Magic link error:", e);
776
+ return jsonResponse({ error: e.message || "Failed to create magic link" }, 500);
777
+ }
778
+ }),
779
+ });
780
+
781
+ http.route({
782
+ path: "/workspace/magic-link",
783
+ method: "OPTIONS",
784
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
785
+ });
786
+
787
+ // Poll magic link status (for agents to check if user clicked)
788
+ http.route({
789
+ path: "/workspace/poll",
790
+ method: "GET",
791
+ handler: httpAction(async (ctx, request) => {
792
+ const url = new URL(request.url);
793
+ const token = url.searchParams.get("token");
794
+
795
+ if (!token) {
796
+ return jsonResponse({ error: "token required" }, 400);
797
+ }
798
+
799
+ const result = await ctx.runQuery(api.workspaces.pollMagicLink, { token });
800
+ return jsonResponse(result);
801
+ }),
802
+ });
803
+
804
+ http.route({
805
+ path: "/workspace/poll",
806
+ method: "OPTIONS",
807
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
808
+ });
809
+
810
+ // Verify session token
811
+ http.route({
812
+ path: "/workspace/verify-session",
813
+ method: "GET",
814
+ handler: httpAction(async (ctx, request) => {
815
+ const url = new URL(request.url);
816
+ const sessionToken = url.searchParams.get("sessionToken");
817
+
818
+ if (!sessionToken) {
819
+ return jsonResponse({ error: "sessionToken required" }, 400);
820
+ }
821
+
822
+ const result = await ctx.runQuery(api.workspaces.verifySession, { sessionToken });
823
+
824
+ if (!result) {
825
+ return jsonResponse({ error: "Invalid or expired session" }, 401);
826
+ }
827
+
828
+ return jsonResponse(result);
829
+ }),
830
+ });
831
+
832
+ http.route({
833
+ path: "/workspace/verify-session",
834
+ method: "OPTIONS",
835
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
836
+ });
837
+
838
+ // Get workspace by email
839
+ http.route({
840
+ path: "/workspace/by-email",
841
+ method: "GET",
842
+ handler: httpAction(async (ctx, request) => {
843
+ const url = new URL(request.url);
844
+ const email = url.searchParams.get("email");
845
+
846
+ if (!email) {
847
+ return jsonResponse({ error: "email required" }, 400);
848
+ }
849
+
850
+ const result = await ctx.runQuery(api.workspaces.getByEmail, { email });
851
+
852
+ if (!result) {
853
+ return jsonResponse({ exists: false });
854
+ }
855
+
856
+ return jsonResponse({ exists: true, workspace: result });
857
+ }),
858
+ });
859
+
860
+ http.route({
861
+ path: "/workspace/by-email",
862
+ method: "OPTIONS",
863
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
864
+ });
865
+
866
+ // Send reminder email
867
+ http.route({
868
+ path: "/workspace/send-reminder",
869
+ method: "POST",
870
+ handler: httpAction(async (ctx, request) => {
871
+ try {
872
+ const body = await request.json();
873
+ const { email, token } = body;
874
+
875
+ if (!email || !token) {
876
+ return jsonResponse({ error: "email and token required" }, 400);
877
+ }
878
+
879
+ await ctx.runAction(api.email.sendReminderEmail, { email, token });
880
+ return jsonResponse({ success: true });
881
+ } catch (e: any) {
882
+ return jsonResponse({ error: e.message }, 500);
883
+ }
884
+ }),
885
+ });
886
+
887
+ http.route({
888
+ path: "/workspace/send-reminder",
889
+ method: "OPTIONS",
890
+ handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
891
+ });
892
+
893
+ // ==============================================
894
+ // STRIPE BILLING ENDPOINTS
895
+ // ==============================================
896
+
897
+ // Create checkout session
898
+ http.route({
899
+ path: "/api/billing/checkout",
900
+ method: "POST",
901
+ handler: createCheckoutSession,
902
+ });
903
+
904
+ http.route({
905
+ path: "/api/billing/checkout",
906
+ method: "OPTIONS",
907
+ handler: checkoutOptions,
908
+ });
909
+
910
+ // Create billing portal session
911
+ http.route({
912
+ path: "/api/billing/portal",
913
+ method: "POST",
914
+ handler: createPortalSession,
915
+ });
916
+
917
+ http.route({
918
+ path: "/api/billing/portal",
919
+ method: "OPTIONS",
920
+ handler: portalOptions,
921
+ });
922
+
923
+ // Stripe webhook handler
924
+ http.route({
925
+ path: "/api/webhooks/stripe",
926
+ method: "POST",
927
+ handler: handleStripeWebhook,
928
+ });
929
+
930
+ http.route({
931
+ path: "/api/webhooks/stripe",
932
+ method: "OPTIONS",
933
+ handler: webhookOptions,
934
+ });