@nordsym/apiclaw 1.5.18 → 1.6.0

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 (141) hide show
  1. package/CODEBASE.md +42 -0
  2. package/CONCEPT.md +9 -0
  3. package/DASHBOARD_FIX.md +9 -0
  4. package/EARN-CREDITS-SPEC.md +9 -0
  5. package/HIVR-INTEGRATION.md +9 -0
  6. package/HTTP-API.md +9 -0
  7. package/PRD-ANALYTICS-AGENTS-TEAMS.md +9 -0
  8. package/PRD-API-CHAINING.md +9 -0
  9. package/PRD-HARDEN-SHELL.md +9 -0
  10. package/PRD-LOGS-SUBAGENTS-V2.md +9 -0
  11. package/VISION.md +9 -0
  12. package/{AGENTS.md → apiclaw-AGENTS.md} +9 -0
  13. package/{CHANGELOG.md → apiclaw-CHANGELOG.md} +31 -0
  14. package/{CONTRIBUTING.md → apiclaw-CONTRIBUTING.md} +9 -0
  15. package/{HEARTBEAT.md → apiclaw-HEARTBEAT.md} +9 -0
  16. package/{IDENTITY.md → apiclaw-IDENTITY.md} +9 -0
  17. package/{README.md → apiclaw-README.md} +9 -0
  18. package/{SOUL.md → apiclaw-SOUL.md} +9 -0
  19. package/{TOOLS.md → apiclaw-TOOLS.md} +9 -0
  20. package/{USER.md → apiclaw-USER.md} +9 -0
  21. package/convex/_generated/api.d.ts +2 -0
  22. package/convex/{README.md → apiclaw-convex-README.md} +9 -0
  23. package/convex/http.ts +315 -0
  24. package/convex/seedPratham.ts +161 -0
  25. package/dist/credentials.d.ts.map +1 -1
  26. package/dist/credentials.js +136 -2
  27. package/dist/credentials.js.map +1 -1
  28. package/dist/execute.d.ts.map +1 -1
  29. package/dist/execute.js +289 -1
  30. package/dist/execute.js.map +1 -1
  31. package/docs/PRD-BILLING.md +9 -0
  32. package/docs/PRD-EARN-SYSTEM.md +9 -0
  33. package/docs/PRD-MCP-AUTO-SETUP.md +9 -0
  34. package/docs/PRD-ORGANIC-DISTRIBUTION.md +9 -0
  35. package/docs/PRD-agent-first-billing.md +9 -0
  36. package/docs/PRD-customer-key-passthrough.md +9 -0
  37. package/docs/PRD-final-polish.md +9 -0
  38. package/docs/PRD-mobile-responsive.md +9 -0
  39. package/docs/PRD-navigation-expansion.md +9 -0
  40. package/docs/PRD-stripe-billing.md +9 -0
  41. package/docs/PRD-workspace-cleanup.md +9 -0
  42. package/docs/PRD-workspace-fixes.md +9 -0
  43. package/docs/SUBAGENT-NAMING.md +9 -0
  44. package/docs/enterprise-deployment.md +6 -0
  45. package/email-templates/{README.md → email-templates-README.md} +9 -0
  46. package/landing/DESIGN.md +9 -0
  47. package/package.json +2 -2
  48. package/scripts/SYMBOT-FIX.md +9 -0
  49. package/src/credentials.ts +150 -2
  50. package/src/execute.ts +306 -1
  51. package/test-legacy-apis.sh +51 -0
  52. package/convex/adminActivate.d.ts +0 -3
  53. package/convex/adminActivate.js +0 -47
  54. package/convex/adminActivate.js.map +0 -1
  55. package/convex/adminStats.d.ts +0 -3
  56. package/convex/adminStats.js +0 -42
  57. package/convex/adminStats.js.map +0 -1
  58. package/convex/agents.d.ts +0 -54
  59. package/convex/agents.js +0 -499
  60. package/convex/agents.js.map +0 -1
  61. package/convex/analytics.d.ts +0 -5
  62. package/convex/analytics.js +0 -166
  63. package/convex/analytics.js.map +0 -1
  64. package/convex/billing.d.ts +0 -88
  65. package/convex/billing.js +0 -655
  66. package/convex/billing.js.map +0 -1
  67. package/convex/capabilities.d.ts +0 -9
  68. package/convex/capabilities.js +0 -145
  69. package/convex/capabilities.js.map +0 -1
  70. package/convex/chains.d.ts +0 -67
  71. package/convex/chains.js +0 -1042
  72. package/convex/chains.js.map +0 -1
  73. package/convex/credits.d.ts +0 -25
  74. package/convex/credits.js +0 -186
  75. package/convex/credits.js.map +0 -1
  76. package/convex/crons.d.ts +0 -3
  77. package/convex/crons.js +0 -17
  78. package/convex/crons.js.map +0 -1
  79. package/convex/directCall.d.ts +0 -72
  80. package/convex/directCall.js +0 -627
  81. package/convex/directCall.js.map +0 -1
  82. package/convex/earnProgress.d.ts +0 -58
  83. package/convex/earnProgress.js +0 -649
  84. package/convex/earnProgress.js.map +0 -1
  85. package/convex/email.d.ts +0 -14
  86. package/convex/email.js +0 -300
  87. package/convex/email.js.map +0 -1
  88. package/convex/feedback.d.ts +0 -7
  89. package/convex/feedback.js +0 -227
  90. package/convex/feedback.js.map +0 -1
  91. package/convex/http.d.ts +0 -3
  92. package/convex/http.js +0 -1106
  93. package/convex/http.js.map +0 -1
  94. package/convex/http.ts.bak +0 -934
  95. package/convex/logs.d.ts +0 -38
  96. package/convex/logs.js +0 -487
  97. package/convex/logs.js.map +0 -1
  98. package/convex/mou.d.ts +0 -6
  99. package/convex/mou.js +0 -82
  100. package/convex/mou.js.map +0 -1
  101. package/convex/providerKeys.d.ts +0 -31
  102. package/convex/providerKeys.js +0 -257
  103. package/convex/providerKeys.js.map +0 -1
  104. package/convex/providers.d.ts +0 -29
  105. package/convex/providers.js +0 -756
  106. package/convex/providers.js.map +0 -1
  107. package/convex/purchases.d.ts +0 -7
  108. package/convex/purchases.js +0 -157
  109. package/convex/purchases.js.map +0 -1
  110. package/convex/ratelimit.d.ts +0 -4
  111. package/convex/ratelimit.js +0 -91
  112. package/convex/ratelimit.js.map +0 -1
  113. package/convex/searchLogs.d.ts +0 -4
  114. package/convex/searchLogs.js +0 -129
  115. package/convex/searchLogs.js.map +0 -1
  116. package/convex/spendAlerts.d.ts +0 -36
  117. package/convex/spendAlerts.js +0 -380
  118. package/convex/spendAlerts.js.map +0 -1
  119. package/convex/stripeActions.d.ts +0 -19
  120. package/convex/stripeActions.js +0 -411
  121. package/convex/stripeActions.js.map +0 -1
  122. package/convex/teams.d.ts +0 -21
  123. package/convex/teams.js +0 -215
  124. package/convex/teams.js.map +0 -1
  125. package/convex/telemetry.d.ts +0 -4
  126. package/convex/telemetry.js +0 -74
  127. package/convex/telemetry.js.map +0 -1
  128. package/convex/usage.d.ts +0 -27
  129. package/convex/usage.js +0 -229
  130. package/convex/usage.js.map +0 -1
  131. package/convex/waitlist.d.ts +0 -4
  132. package/convex/waitlist.js +0 -49
  133. package/convex/waitlist.js.map +0 -1
  134. package/convex/webhooks.d.ts +0 -12
  135. package/convex/webhooks.js +0 -410
  136. package/convex/webhooks.js.map +0 -1
  137. package/convex/workspaces.d.ts +0 -29
  138. package/convex/workspaces.js +0 -880
  139. package/convex/workspaces.js.map +0 -1
  140. package/dist/registry/apis.json.bak +0 -248811
  141. package/src/registry/apis.json.bak +0 -248811
@@ -1,934 +0,0 @@
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
- });