@nordsym/apiclaw 1.7.2 → 1.7.4
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.
- package/convex/_generated/api.d.ts +115 -0
- package/convex/_generated/api.js +23 -0
- package/convex/_generated/dataModel.d.ts +60 -0
- package/convex/_generated/server.d.ts +143 -0
- package/convex/_generated/server.js +93 -0
- package/convex/adminActivate.d.ts +3 -0
- package/convex/adminActivate.d.ts.map +1 -0
- package/convex/adminActivate.js +47 -0
- package/convex/adminActivate.js.map +1 -0
- package/convex/adminActivate.ts +54 -0
- package/convex/adminStats.d.ts +3 -0
- package/convex/adminStats.d.ts.map +1 -0
- package/convex/adminStats.js +42 -0
- package/convex/adminStats.js.map +1 -0
- package/convex/adminStats.ts +44 -0
- package/convex/agents.d.ts +76 -0
- package/convex/agents.d.ts.map +1 -0
- package/convex/agents.js +699 -0
- package/convex/agents.js.map +1 -0
- package/convex/agents.ts +814 -0
- package/convex/analytics.d.ts +5 -0
- package/convex/analytics.d.ts.map +1 -0
- package/convex/analytics.js +166 -0
- package/convex/analytics.js.map +1 -0
- package/convex/analytics.ts +186 -0
- package/convex/billing.d.ts +88 -0
- package/convex/billing.d.ts.map +1 -0
- package/convex/billing.js +655 -0
- package/convex/billing.js.map +1 -0
- package/convex/billing.ts +791 -0
- package/convex/capabilities.d.ts +9 -0
- package/convex/capabilities.d.ts.map +1 -0
- package/convex/capabilities.js +145 -0
- package/convex/capabilities.js.map +1 -0
- package/convex/capabilities.ts +157 -0
- package/convex/chains.d.ts +68 -0
- package/convex/chains.d.ts.map +1 -0
- package/convex/chains.js +1105 -0
- package/convex/chains.js.map +1 -0
- package/convex/chains.ts +1318 -0
- package/convex/credits.d.ts +25 -0
- package/convex/credits.d.ts.map +1 -0
- package/convex/credits.js +186 -0
- package/convex/credits.js.map +1 -0
- package/convex/credits.ts +211 -0
- package/convex/crons.d.ts +3 -0
- package/convex/crons.d.ts.map +1 -0
- package/convex/crons.js +17 -0
- package/convex/crons.js.map +1 -0
- package/convex/crons.ts +28 -0
- package/convex/directCall.d.ts +72 -0
- package/convex/directCall.d.ts.map +1 -0
- package/convex/directCall.js +627 -0
- package/convex/directCall.js.map +1 -0
- package/convex/directCall.ts +678 -0
- package/convex/earnProgress.d.ts +58 -0
- package/convex/earnProgress.d.ts.map +1 -0
- package/convex/earnProgress.js +649 -0
- package/convex/earnProgress.js.map +1 -0
- package/convex/earnProgress.ts +753 -0
- package/convex/email.d.ts +14 -0
- package/convex/email.d.ts.map +1 -0
- package/convex/email.js +300 -0
- package/convex/email.js.map +1 -0
- package/convex/email.ts +329 -0
- package/convex/feedback.d.ts +7 -0
- package/convex/feedback.d.ts.map +1 -0
- package/convex/feedback.js +227 -0
- package/convex/feedback.js.map +1 -0
- package/convex/feedback.ts +265 -0
- package/convex/http.d.ts +3 -0
- package/convex/http.d.ts.map +1 -0
- package/convex/http.js +1405 -0
- package/convex/http.js.map +1 -0
- package/convex/http.ts +1577 -0
- package/convex/inbound.d.ts +2 -0
- package/convex/inbound.d.ts.map +1 -0
- package/convex/inbound.js +32 -0
- package/convex/inbound.js.map +1 -0
- package/convex/inbound.ts +32 -0
- package/convex/logs.d.ts +38 -0
- package/convex/logs.d.ts.map +1 -0
- package/convex/logs.js +487 -0
- package/convex/logs.js.map +1 -0
- package/convex/logs.ts +550 -0
- package/convex/mou.d.ts +6 -0
- package/convex/mou.d.ts.map +1 -0
- package/convex/mou.js +82 -0
- package/convex/mou.js.map +1 -0
- package/convex/mou.ts +91 -0
- package/convex/providerKeys.d.ts +31 -0
- package/convex/providerKeys.d.ts.map +1 -0
- package/convex/providerKeys.js +257 -0
- package/convex/providerKeys.js.map +1 -0
- package/convex/providerKeys.ts +289 -0
- package/convex/providers.d.ts +32 -0
- package/convex/providers.d.ts.map +1 -0
- package/convex/providers.js +814 -0
- package/convex/providers.js.map +1 -0
- package/convex/providers.ts +909 -0
- package/convex/purchases.d.ts +7 -0
- package/convex/purchases.d.ts.map +1 -0
- package/convex/purchases.js +157 -0
- package/convex/purchases.js.map +1 -0
- package/convex/purchases.ts +183 -0
- package/convex/ratelimit.d.ts +4 -0
- package/convex/ratelimit.d.ts.map +1 -0
- package/convex/ratelimit.js +91 -0
- package/convex/ratelimit.js.map +1 -0
- package/convex/ratelimit.ts +104 -0
- package/convex/schema.ts +802 -0
- package/convex/searchLogs.d.ts +4 -0
- package/convex/searchLogs.d.ts.map +1 -0
- package/convex/searchLogs.js +129 -0
- package/convex/searchLogs.js.map +1 -0
- package/convex/searchLogs.ts +146 -0
- package/convex/seedAPILayerAPIs.d.ts +7 -0
- package/convex/seedAPILayerAPIs.d.ts.map +1 -0
- package/convex/seedAPILayerAPIs.js +177 -0
- package/convex/seedAPILayerAPIs.js.map +1 -0
- package/convex/seedAPILayerAPIs.ts +191 -0
- package/convex/seedDirectCallConfigs.d.ts +2 -0
- package/convex/seedDirectCallConfigs.d.ts.map +1 -0
- package/convex/seedDirectCallConfigs.js +324 -0
- package/convex/seedDirectCallConfigs.js.map +1 -0
- package/convex/seedDirectCallConfigs.ts +336 -0
- package/convex/seedPratham.d.ts +6 -0
- package/convex/seedPratham.d.ts.map +1 -0
- package/convex/seedPratham.js +150 -0
- package/convex/seedPratham.js.map +1 -0
- package/convex/seedPratham.ts +161 -0
- package/convex/spendAlerts.d.ts +36 -0
- package/convex/spendAlerts.d.ts.map +1 -0
- package/convex/spendAlerts.js +380 -0
- package/convex/spendAlerts.js.map +1 -0
- package/convex/spendAlerts.ts +442 -0
- package/convex/stripeActions.d.ts +19 -0
- package/convex/stripeActions.d.ts.map +1 -0
- package/convex/stripeActions.js +411 -0
- package/convex/stripeActions.js.map +1 -0
- package/convex/stripeActions.ts +512 -0
- package/convex/teams.d.ts +21 -0
- package/convex/teams.d.ts.map +1 -0
- package/convex/teams.js +215 -0
- package/convex/teams.js.map +1 -0
- package/convex/teams.ts +243 -0
- package/convex/telemetry.d.ts +4 -0
- package/convex/telemetry.d.ts.map +1 -0
- package/convex/telemetry.js +74 -0
- package/convex/telemetry.js.map +1 -0
- package/convex/telemetry.ts +81 -0
- package/convex/tsconfig.json +25 -0
- package/convex/updateAPIStatus.d.ts +6 -0
- package/convex/updateAPIStatus.d.ts.map +1 -0
- package/convex/updateAPIStatus.js +40 -0
- package/convex/updateAPIStatus.js.map +1 -0
- package/convex/updateAPIStatus.ts +45 -0
- package/convex/usage.d.ts +27 -0
- package/convex/usage.d.ts.map +1 -0
- package/convex/usage.js +229 -0
- package/convex/usage.js.map +1 -0
- package/convex/usage.ts +260 -0
- package/convex/waitlist.d.ts +4 -0
- package/convex/waitlist.d.ts.map +1 -0
- package/convex/waitlist.js +49 -0
- package/convex/waitlist.js.map +1 -0
- package/convex/waitlist.ts +55 -0
- package/convex/webhooks.d.ts +12 -0
- package/convex/webhooks.d.ts.map +1 -0
- package/convex/webhooks.js +410 -0
- package/convex/webhooks.js.map +1 -0
- package/convex/webhooks.ts +494 -0
- package/convex/workspaces.d.ts +31 -0
- package/convex/workspaces.d.ts.map +1 -0
- package/convex/workspaces.js +975 -0
- package/convex/workspaces.js.map +1 -0
- package/convex/workspaces.ts +1130 -0
- package/dist/bin.js +0 -0
- package/dist/capability-router.js +1 -1
- package/dist/capability-router.js.map +1 -1
- package/dist/execute.d.ts +2 -0
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +18 -4
- package/dist/execute.js.map +1 -1
- package/dist/http-api.js +1 -1
- package/dist/http-api.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-analytics.d.ts +32 -0
- package/dist/mcp-analytics.d.ts.map +1 -0
- package/dist/mcp-analytics.js +130 -0
- package/dist/mcp-analytics.js.map +1 -0
- package/package.json +1 -1
- package/dist/chain-types.d.ts +0 -187
- package/dist/chain-types.d.ts.map +0 -1
- package/dist/chain-types.js +0 -33
- package/dist/chain-types.js.map +0 -1
- package/dist/registry/apis.json.bak +0 -248811
- package/dist/src/bin.js +0 -17
- package/dist/src/capability-router.js +0 -240
- package/dist/src/chainExecutor.js +0 -451
- package/dist/src/chainResolver.js +0 -518
- package/dist/src/cli/commands/doctor.js +0 -324
- package/dist/src/cli/commands/mcp-install.js +0 -255
- package/dist/src/cli/commands/restore.js +0 -259
- package/dist/src/cli/commands/setup.js +0 -205
- package/dist/src/cli/commands/uninstall.js +0 -188
- package/dist/src/cli/index.js +0 -111
- package/dist/src/cli.js +0 -302
- package/dist/src/confirmation.js +0 -240
- package/dist/src/credentials.js +0 -357
- package/dist/src/credits.js +0 -260
- package/dist/src/crypto.js +0 -66
- package/dist/src/discovery.js +0 -504
- package/dist/src/enterprise/env.js +0 -123
- package/dist/src/enterprise/script-generator.js +0 -460
- package/dist/src/execute-dynamic.js +0 -473
- package/dist/src/execute.js +0 -1727
- package/dist/src/index.js +0 -2062
- package/dist/src/metered.js +0 -80
- package/dist/src/open-apis.js +0 -276
- package/dist/src/proxy.js +0 -28
- package/dist/src/session.js +0 -86
- package/dist/src/stripe.js +0 -407
- package/dist/src/telemetry.js +0 -49
- package/dist/src/types.js +0 -2
- package/dist/src/utils/backup.js +0 -181
- package/dist/src/utils/config.js +0 -220
- package/dist/src/utils/os.js +0 -105
- package/dist/src/utils/paths.js +0 -159
package/convex/http.js
ADDED
|
@@ -0,0 +1,1405 @@
|
|
|
1
|
+
import { httpRouter } from "convex/server";
|
|
2
|
+
import { httpAction } from "./_generated/server";
|
|
3
|
+
import { api, internal } from "./_generated/api";
|
|
4
|
+
import { createCheckoutSession, createPortalSession, handleStripeWebhook, checkoutOptions, portalOptions, webhookOptions, } from "./stripeActions";
|
|
5
|
+
const http = httpRouter();
|
|
6
|
+
// Provider catalog
|
|
7
|
+
const PROVIDERS = {
|
|
8
|
+
"46elks": {
|
|
9
|
+
name: "46elks",
|
|
10
|
+
description: "SMS API for EU/Nordics. GDPR compliant.",
|
|
11
|
+
category: "sms",
|
|
12
|
+
pricing: "~$0.035/SMS",
|
|
13
|
+
regions: ["EU", "Nordic"],
|
|
14
|
+
tags: ["sms", "eu", "gdpr", "nordic"],
|
|
15
|
+
},
|
|
16
|
+
twilio: {
|
|
17
|
+
name: "Twilio",
|
|
18
|
+
description: "SMS and Voice API. Global coverage.",
|
|
19
|
+
category: "sms",
|
|
20
|
+
pricing: "~$0.04/SMS, ~$0.01/min voice",
|
|
21
|
+
regions: ["Global"],
|
|
22
|
+
tags: ["sms", "voice", "global"],
|
|
23
|
+
},
|
|
24
|
+
resend: {
|
|
25
|
+
name: "Resend",
|
|
26
|
+
description: "Modern email API. Developer-friendly.",
|
|
27
|
+
category: "email",
|
|
28
|
+
pricing: "~$0.001/email",
|
|
29
|
+
regions: ["Global"],
|
|
30
|
+
tags: ["email", "transactional"],
|
|
31
|
+
},
|
|
32
|
+
brave_search: {
|
|
33
|
+
name: "Brave Search",
|
|
34
|
+
description: "Privacy-focused web search API.",
|
|
35
|
+
category: "search",
|
|
36
|
+
pricing: "~$0.005/search",
|
|
37
|
+
regions: ["Global"],
|
|
38
|
+
tags: ["search", "web", "privacy"],
|
|
39
|
+
},
|
|
40
|
+
openrouter: {
|
|
41
|
+
name: "OpenRouter",
|
|
42
|
+
description: "Multi-model LLM API. Access GPT, Claude, Llama, etc.",
|
|
43
|
+
category: "llm",
|
|
44
|
+
pricing: "Varies by model",
|
|
45
|
+
regions: ["Global"],
|
|
46
|
+
tags: ["llm", "ai", "gpt", "claude"],
|
|
47
|
+
},
|
|
48
|
+
elevenlabs: {
|
|
49
|
+
name: "ElevenLabs",
|
|
50
|
+
description: "Text-to-speech API. High quality voices.",
|
|
51
|
+
category: "tts",
|
|
52
|
+
pricing: "~$0.0003/char",
|
|
53
|
+
regions: ["Global"],
|
|
54
|
+
tags: ["tts", "voice", "audio"],
|
|
55
|
+
},
|
|
56
|
+
replicate: {
|
|
57
|
+
name: "Replicate",
|
|
58
|
+
description: "Run AI models (Whisper, SDXL, Llama, etc). Pay per prediction.",
|
|
59
|
+
category: "ai",
|
|
60
|
+
pricing: "Varies by model",
|
|
61
|
+
regions: ["Global"],
|
|
62
|
+
tags: ["ai", "ml", "whisper", "image", "audio", "transcription"],
|
|
63
|
+
},
|
|
64
|
+
firecrawl: {
|
|
65
|
+
name: "Firecrawl",
|
|
66
|
+
description: "Web scraping and crawling API. Extract clean data from any URL.",
|
|
67
|
+
category: "scraping",
|
|
68
|
+
pricing: "~$0.001/page",
|
|
69
|
+
regions: ["Global"],
|
|
70
|
+
tags: ["scraping", "web", "crawl", "extract"],
|
|
71
|
+
},
|
|
72
|
+
github: {
|
|
73
|
+
name: "GitHub",
|
|
74
|
+
description: "GitHub API. Search repos, manage code, access developer data.",
|
|
75
|
+
category: "code",
|
|
76
|
+
pricing: "Free tier available",
|
|
77
|
+
regions: ["Global"],
|
|
78
|
+
tags: ["github", "code", "repos", "developer"],
|
|
79
|
+
},
|
|
80
|
+
e2b: {
|
|
81
|
+
name: "E2B",
|
|
82
|
+
description: "Secure code sandbox for AI agents. Run Python, shell commands in isolated environments.",
|
|
83
|
+
category: "sandbox",
|
|
84
|
+
pricing: "$0.000028/s (2 vCPU)",
|
|
85
|
+
regions: ["Global"],
|
|
86
|
+
tags: ["sandbox", "code", "python", "execution", "ai", "agents"],
|
|
87
|
+
},
|
|
88
|
+
apilayer: {
|
|
89
|
+
name: "APILayer",
|
|
90
|
+
description: "14 APIs: exchange rates, market data, aviation, PDF, screenshots, email/phone verification, VAT, news, scraping, and more.",
|
|
91
|
+
category: "multi",
|
|
92
|
+
pricing: "Free tier available, paid plans per API",
|
|
93
|
+
regions: ["Global"],
|
|
94
|
+
tags: ["exchange", "stocks", "aviation", "pdf", "screenshot", "verification", "vat", "news", "scraping"],
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
// CORS headers
|
|
98
|
+
const corsHeaders = {
|
|
99
|
+
"Access-Control-Allow-Origin": "*",
|
|
100
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
101
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
102
|
+
};
|
|
103
|
+
// Helper for JSON responses
|
|
104
|
+
function jsonResponse(data, status = 200) {
|
|
105
|
+
return new Response(JSON.stringify(data), {
|
|
106
|
+
status,
|
|
107
|
+
headers: { "Content-Type": "application/json", ...corsHeaders },
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
// Helper to validate session and log API usage
|
|
111
|
+
async function validateAndLogProxyCall(ctx, request, provider, action) {
|
|
112
|
+
const identifier = request.headers.get("X-APIClaw-Identifier");
|
|
113
|
+
const subagentId = request.headers.get("X-APIClaw-Subagent") || "main";
|
|
114
|
+
console.log("[Proxy] Call received", { provider, action, identifier, subagentId });
|
|
115
|
+
// ALWAYS log to analytics (even if identifier is missing)
|
|
116
|
+
try {
|
|
117
|
+
const result = await ctx.runMutation(api.analytics.log, {
|
|
118
|
+
event: "api_call",
|
|
119
|
+
provider,
|
|
120
|
+
identifier: identifier || "unknown",
|
|
121
|
+
metadata: { action, subagentId },
|
|
122
|
+
});
|
|
123
|
+
console.log("[Proxy] Analytics logged:", result);
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
console.error("[Proxy] Analytics logging failed:", e.message, e.stack);
|
|
127
|
+
// Continue even if analytics fails
|
|
128
|
+
}
|
|
129
|
+
// If we have an identifier and it's a workspace ID (not anon:), log to workspace
|
|
130
|
+
if (identifier && !identifier.startsWith("anon:") && identifier !== "unknown") {
|
|
131
|
+
try {
|
|
132
|
+
// Validate it's actually a workspace ID by checking format
|
|
133
|
+
if (identifier.length > 20) {
|
|
134
|
+
await ctx.runMutation(api.logs.createProxyLog, {
|
|
135
|
+
workspaceId: identifier,
|
|
136
|
+
provider,
|
|
137
|
+
action,
|
|
138
|
+
subagentId,
|
|
139
|
+
});
|
|
140
|
+
// Increment workspace usage
|
|
141
|
+
await ctx.runMutation(api.workspaces.incrementUsage, {
|
|
142
|
+
workspaceId: identifier,
|
|
143
|
+
});
|
|
144
|
+
console.log("[Proxy] Workspace logged for:", identifier);
|
|
145
|
+
return { valid: true, workspaceId: identifier, subagentId };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch (e) {
|
|
149
|
+
console.error("[Proxy] Workspace logging failed:", e.message);
|
|
150
|
+
// Continue even if workspace logging fails
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Return success regardless (don't block API calls)
|
|
154
|
+
return { valid: true, subagentId };
|
|
155
|
+
}
|
|
156
|
+
// OPTIONS handler for CORS
|
|
157
|
+
http.route({
|
|
158
|
+
path: "/api/discover",
|
|
159
|
+
method: "OPTIONS",
|
|
160
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
161
|
+
});
|
|
162
|
+
http.route({
|
|
163
|
+
path: "/api/details",
|
|
164
|
+
method: "OPTIONS",
|
|
165
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
166
|
+
});
|
|
167
|
+
http.route({
|
|
168
|
+
path: "/api/balance",
|
|
169
|
+
method: "OPTIONS",
|
|
170
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
171
|
+
});
|
|
172
|
+
http.route({
|
|
173
|
+
path: "/api/purchase",
|
|
174
|
+
method: "OPTIONS",
|
|
175
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
176
|
+
});
|
|
177
|
+
http.route({
|
|
178
|
+
path: "/admin/grant-credits",
|
|
179
|
+
method: "OPTIONS",
|
|
180
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
181
|
+
});
|
|
182
|
+
// Discover APIs
|
|
183
|
+
http.route({
|
|
184
|
+
path: "/api/discover",
|
|
185
|
+
method: "POST",
|
|
186
|
+
handler: httpAction(async (ctx, request) => {
|
|
187
|
+
try {
|
|
188
|
+
const startTime = Date.now();
|
|
189
|
+
const body = await request.json();
|
|
190
|
+
const query = (body.query || "").toLowerCase();
|
|
191
|
+
// Get optional auth context
|
|
192
|
+
const sessionToken = request.headers.get("X-APIClaw-Session");
|
|
193
|
+
const userAgent = request.headers.get("User-Agent");
|
|
194
|
+
const results = Object.entries(PROVIDERS)
|
|
195
|
+
.filter(([id, provider]) => {
|
|
196
|
+
if (!query)
|
|
197
|
+
return true;
|
|
198
|
+
return (provider.name.toLowerCase().includes(query) ||
|
|
199
|
+
provider.description.toLowerCase().includes(query) ||
|
|
200
|
+
provider.category.toLowerCase().includes(query) ||
|
|
201
|
+
provider.tags.some((tag) => tag.includes(query)));
|
|
202
|
+
})
|
|
203
|
+
.map(([id, provider]) => ({
|
|
204
|
+
providerId: id,
|
|
205
|
+
...provider,
|
|
206
|
+
}));
|
|
207
|
+
const responseTimeMs = Date.now() - startTime;
|
|
208
|
+
// Log the search (fire and forget)
|
|
209
|
+
if (query) {
|
|
210
|
+
ctx.runMutation(internal.searchLogs.logSearch, {
|
|
211
|
+
query: body.query || "", // Original query (not lowercased)
|
|
212
|
+
resultsCount: results.length,
|
|
213
|
+
matchedProviders: results.map(r => r.providerId),
|
|
214
|
+
sessionToken: sessionToken || undefined,
|
|
215
|
+
userAgent: userAgent || undefined,
|
|
216
|
+
responseTimeMs,
|
|
217
|
+
}).catch(() => { }); // Ignore errors, don't block response
|
|
218
|
+
}
|
|
219
|
+
return jsonResponse({ providers: results, total: results.length });
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
return jsonResponse({ error: "Invalid request" }, 400);
|
|
223
|
+
}
|
|
224
|
+
}),
|
|
225
|
+
});
|
|
226
|
+
// Get provider details
|
|
227
|
+
http.route({
|
|
228
|
+
path: "/api/details",
|
|
229
|
+
method: "POST",
|
|
230
|
+
handler: httpAction(async (ctx, request) => {
|
|
231
|
+
try {
|
|
232
|
+
const body = await request.json();
|
|
233
|
+
const { providerId } = body;
|
|
234
|
+
if (!providerId) {
|
|
235
|
+
return jsonResponse({ error: "providerId required" }, 400);
|
|
236
|
+
}
|
|
237
|
+
const provider = PROVIDERS[providerId];
|
|
238
|
+
if (!provider) {
|
|
239
|
+
return jsonResponse({ error: "Provider not found" }, 404);
|
|
240
|
+
}
|
|
241
|
+
return jsonResponse({
|
|
242
|
+
providerId,
|
|
243
|
+
...provider,
|
|
244
|
+
creditsPerDollar: getCreditsPerDollar(providerId),
|
|
245
|
+
documentation: `https://apiclaw.com/docs/${providerId}`,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
catch (e) {
|
|
249
|
+
return jsonResponse({ error: "Invalid request" }, 400);
|
|
250
|
+
}
|
|
251
|
+
}),
|
|
252
|
+
});
|
|
253
|
+
// Check balance
|
|
254
|
+
http.route({
|
|
255
|
+
path: "/api/balance",
|
|
256
|
+
method: "GET",
|
|
257
|
+
handler: httpAction(async (ctx, request) => {
|
|
258
|
+
const url = new URL(request.url);
|
|
259
|
+
const agentId = url.searchParams.get("agentId");
|
|
260
|
+
if (!agentId) {
|
|
261
|
+
return jsonResponse({ error: "agentId required" }, 400);
|
|
262
|
+
}
|
|
263
|
+
const credits = await ctx.runQuery(api.credits.getAgentCredits, { agentId });
|
|
264
|
+
if (!credits) {
|
|
265
|
+
return jsonResponse({
|
|
266
|
+
agentId,
|
|
267
|
+
balanceUsd: 0,
|
|
268
|
+
currency: "USD",
|
|
269
|
+
message: "No account found. Top up to get started!",
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return jsonResponse({
|
|
273
|
+
agentId: credits.agentId,
|
|
274
|
+
balanceUsd: credits.balanceUsd,
|
|
275
|
+
currency: credits.currency,
|
|
276
|
+
});
|
|
277
|
+
}),
|
|
278
|
+
});
|
|
279
|
+
// Purchase API access
|
|
280
|
+
http.route({
|
|
281
|
+
path: "/api/purchase",
|
|
282
|
+
method: "POST",
|
|
283
|
+
handler: httpAction(async (ctx, request) => {
|
|
284
|
+
try {
|
|
285
|
+
const body = await request.json();
|
|
286
|
+
const { agentId, providerId, amountUsd } = body;
|
|
287
|
+
if (!agentId || !providerId || !amountUsd) {
|
|
288
|
+
return jsonResponse({ error: "agentId, providerId, and amountUsd required" }, 400);
|
|
289
|
+
}
|
|
290
|
+
if (amountUsd < 1 || amountUsd > 1000) {
|
|
291
|
+
return jsonResponse({ error: "amountUsd must be between 1 and 1000" }, 400);
|
|
292
|
+
}
|
|
293
|
+
const provider = PROVIDERS[providerId];
|
|
294
|
+
if (!provider) {
|
|
295
|
+
return jsonResponse({ error: "Provider not found" }, 404);
|
|
296
|
+
}
|
|
297
|
+
// Check balance first
|
|
298
|
+
const credits = await ctx.runQuery(api.credits.getAgentCredits, { agentId });
|
|
299
|
+
if (!credits || credits.balanceUsd < amountUsd) {
|
|
300
|
+
return jsonResponse({
|
|
301
|
+
error: "Insufficient balance",
|
|
302
|
+
currentBalance: credits?.balanceUsd || 0,
|
|
303
|
+
required: amountUsd,
|
|
304
|
+
}, 402);
|
|
305
|
+
}
|
|
306
|
+
// Execute purchase
|
|
307
|
+
const purchase = await ctx.runMutation(api.purchases.purchaseAccess, {
|
|
308
|
+
agentId,
|
|
309
|
+
providerId,
|
|
310
|
+
amountUsd,
|
|
311
|
+
credentials: generateCredentials(providerId),
|
|
312
|
+
});
|
|
313
|
+
if (!purchase) {
|
|
314
|
+
return jsonResponse({ error: "Purchase failed" }, 500);
|
|
315
|
+
}
|
|
316
|
+
return jsonResponse({
|
|
317
|
+
success: true,
|
|
318
|
+
purchase: {
|
|
319
|
+
id: purchase._id,
|
|
320
|
+
providerId: purchase.providerId,
|
|
321
|
+
amountUsd: purchase.amountUsd,
|
|
322
|
+
creditsGranted: purchase.creditsGranted,
|
|
323
|
+
status: purchase.status,
|
|
324
|
+
},
|
|
325
|
+
message: `Successfully purchased $${amountUsd} of ${provider.name} credits`,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
catch (e) {
|
|
329
|
+
return jsonResponse({ error: e.message || "Purchase failed" }, 400);
|
|
330
|
+
}
|
|
331
|
+
}),
|
|
332
|
+
});
|
|
333
|
+
// Admin: Grant credits
|
|
334
|
+
http.route({
|
|
335
|
+
path: "/admin/grant-credits",
|
|
336
|
+
method: "POST",
|
|
337
|
+
handler: httpAction(async (ctx, request) => {
|
|
338
|
+
try {
|
|
339
|
+
const body = await request.json();
|
|
340
|
+
const { agentId, amount, reason } = body;
|
|
341
|
+
if (!agentId || !amount) {
|
|
342
|
+
return jsonResponse({ error: "agentId and amount required" }, 400);
|
|
343
|
+
}
|
|
344
|
+
// TODO: Add admin auth check here
|
|
345
|
+
// For now, allow grants (this is for Hivr integration)
|
|
346
|
+
const result = await ctx.runMutation(api.credits.addCredits, {
|
|
347
|
+
agentId,
|
|
348
|
+
amountUsd: amount,
|
|
349
|
+
source: reason || "admin_grant",
|
|
350
|
+
});
|
|
351
|
+
return jsonResponse({
|
|
352
|
+
success: true,
|
|
353
|
+
agentId,
|
|
354
|
+
credited: amount,
|
|
355
|
+
newBalance: result?.balanceUsd,
|
|
356
|
+
reason,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
catch (e) {
|
|
360
|
+
return jsonResponse({ error: e.message || "Grant failed" }, 400);
|
|
361
|
+
}
|
|
362
|
+
}),
|
|
363
|
+
});
|
|
364
|
+
// Helper functions
|
|
365
|
+
function getCreditsPerDollar(providerId) {
|
|
366
|
+
const rates = {
|
|
367
|
+
"46elks": 30,
|
|
368
|
+
twilio: 25,
|
|
369
|
+
resend: 1000,
|
|
370
|
+
brave_search: 200,
|
|
371
|
+
openrouter: 100,
|
|
372
|
+
elevenlabs: 3333,
|
|
373
|
+
};
|
|
374
|
+
return rates[providerId] || 100;
|
|
375
|
+
}
|
|
376
|
+
function generateCredentials(providerId) {
|
|
377
|
+
// In production, this would generate or retrieve actual API keys
|
|
378
|
+
// For now, return placeholder indicating how to use
|
|
379
|
+
return {
|
|
380
|
+
type: "apiclaw_proxy",
|
|
381
|
+
endpoint: `https://brilliant-puffin-712.convex.site/proxy/${providerId}`,
|
|
382
|
+
note: "Use APIClaw proxy endpoint. Credentials managed automatically.",
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
export default http;
|
|
386
|
+
// ==============================================
|
|
387
|
+
// DIRECT CALL PROXY ENDPOINTS
|
|
388
|
+
// ==============================================
|
|
389
|
+
// OpenRouter proxy
|
|
390
|
+
http.route({
|
|
391
|
+
path: "/proxy/openrouter",
|
|
392
|
+
method: "POST",
|
|
393
|
+
handler: httpAction(async (ctx, request) => {
|
|
394
|
+
// Validate session and log usage
|
|
395
|
+
await validateAndLogProxyCall(ctx, request, "openrouter", "chat");
|
|
396
|
+
const OPENROUTER_KEY = process.env.OPENROUTER_API_KEY;
|
|
397
|
+
if (!OPENROUTER_KEY) {
|
|
398
|
+
return jsonResponse({ error: "OpenRouter not configured" }, 500);
|
|
399
|
+
}
|
|
400
|
+
try {
|
|
401
|
+
const body = await request.json();
|
|
402
|
+
const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
|
|
403
|
+
method: "POST",
|
|
404
|
+
headers: {
|
|
405
|
+
"Authorization": `Bearer ${OPENROUTER_KEY}`,
|
|
406
|
+
"Content-Type": "application/json",
|
|
407
|
+
"HTTP-Referer": "https://apiclaw.nordsym.com",
|
|
408
|
+
"X-Title": "APIClaw",
|
|
409
|
+
},
|
|
410
|
+
body: JSON.stringify(body),
|
|
411
|
+
});
|
|
412
|
+
const data = await response.json();
|
|
413
|
+
return jsonResponse(data, response.status);
|
|
414
|
+
}
|
|
415
|
+
catch (e) {
|
|
416
|
+
return jsonResponse({ error: e.message }, 500);
|
|
417
|
+
}
|
|
418
|
+
}),
|
|
419
|
+
});
|
|
420
|
+
// Brave Search proxy
|
|
421
|
+
http.route({
|
|
422
|
+
path: "/proxy/brave_search",
|
|
423
|
+
method: "POST",
|
|
424
|
+
handler: httpAction(async (ctx, request) => {
|
|
425
|
+
// Validate session and log usage
|
|
426
|
+
await validateAndLogProxyCall(ctx, request, "brave_search", "search");
|
|
427
|
+
const BRAVE_KEY = process.env.BRAVE_API_KEY;
|
|
428
|
+
if (!BRAVE_KEY) {
|
|
429
|
+
return jsonResponse({ error: "Brave Search not configured" }, 500);
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
const body = await request.json();
|
|
433
|
+
const { query, count = 10 } = body;
|
|
434
|
+
const url = new URL("https://api.search.brave.com/res/v1/web/search");
|
|
435
|
+
url.searchParams.set("q", query);
|
|
436
|
+
url.searchParams.set("count", String(count));
|
|
437
|
+
const response = await fetch(url.toString(), {
|
|
438
|
+
headers: { "X-Subscription-Token": BRAVE_KEY },
|
|
439
|
+
});
|
|
440
|
+
const data = await response.json();
|
|
441
|
+
return jsonResponse(data, response.status);
|
|
442
|
+
}
|
|
443
|
+
catch (e) {
|
|
444
|
+
return jsonResponse({ error: e.message }, 500);
|
|
445
|
+
}
|
|
446
|
+
}),
|
|
447
|
+
});
|
|
448
|
+
// Resend email proxy
|
|
449
|
+
http.route({
|
|
450
|
+
path: "/proxy/resend",
|
|
451
|
+
method: "POST",
|
|
452
|
+
handler: httpAction(async (ctx, request) => {
|
|
453
|
+
// Validate session and log usage
|
|
454
|
+
await validateAndLogProxyCall(ctx, request, "resend", "send_email");
|
|
455
|
+
const RESEND_KEY = process.env.RESEND_API_KEY;
|
|
456
|
+
if (!RESEND_KEY) {
|
|
457
|
+
return jsonResponse({ error: "Resend not configured" }, 500);
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
const body = await request.json();
|
|
461
|
+
const response = await fetch("https://api.resend.com/emails", {
|
|
462
|
+
method: "POST",
|
|
463
|
+
headers: {
|
|
464
|
+
"Authorization": `Bearer ${RESEND_KEY}`,
|
|
465
|
+
"Content-Type": "application/json",
|
|
466
|
+
},
|
|
467
|
+
body: JSON.stringify(body),
|
|
468
|
+
});
|
|
469
|
+
const data = await response.json();
|
|
470
|
+
return jsonResponse(data, response.status);
|
|
471
|
+
}
|
|
472
|
+
catch (e) {
|
|
473
|
+
return jsonResponse({ error: e.message }, 500);
|
|
474
|
+
}
|
|
475
|
+
}),
|
|
476
|
+
});
|
|
477
|
+
// ElevenLabs TTS proxy
|
|
478
|
+
http.route({
|
|
479
|
+
path: "/proxy/elevenlabs",
|
|
480
|
+
method: "POST",
|
|
481
|
+
handler: httpAction(async (ctx, request) => {
|
|
482
|
+
// Validate session and log usage
|
|
483
|
+
await validateAndLogProxyCall(ctx, request, "elevenlabs", "text_to_speech");
|
|
484
|
+
const ELEVENLABS_KEY = process.env.ELEVENLABS_API_KEY;
|
|
485
|
+
if (!ELEVENLABS_KEY) {
|
|
486
|
+
return jsonResponse({ error: "ElevenLabs not configured" }, 500);
|
|
487
|
+
}
|
|
488
|
+
try {
|
|
489
|
+
const body = await request.json();
|
|
490
|
+
const { text, voice_id = "21m00Tcm4TlvDq8ikWAM" } = body;
|
|
491
|
+
const response = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voice_id}`, {
|
|
492
|
+
method: "POST",
|
|
493
|
+
headers: {
|
|
494
|
+
"xi-api-key": ELEVENLABS_KEY,
|
|
495
|
+
"Content-Type": "application/json",
|
|
496
|
+
},
|
|
497
|
+
body: JSON.stringify({
|
|
498
|
+
text,
|
|
499
|
+
model_id: "eleven_turbo_v2",
|
|
500
|
+
}),
|
|
501
|
+
});
|
|
502
|
+
if (!response.ok) {
|
|
503
|
+
const error = await response.text();
|
|
504
|
+
return jsonResponse({ error }, response.status);
|
|
505
|
+
}
|
|
506
|
+
// Return audio as base64
|
|
507
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
508
|
+
const base64 = Buffer.from(arrayBuffer).toString("base64");
|
|
509
|
+
return jsonResponse({
|
|
510
|
+
audio_base64: base64,
|
|
511
|
+
content_type: "audio/mpeg",
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
catch (e) {
|
|
515
|
+
return jsonResponse({ error: e.message }, 500);
|
|
516
|
+
}
|
|
517
|
+
}),
|
|
518
|
+
});
|
|
519
|
+
http.route({
|
|
520
|
+
path: "/proxy/openrouter",
|
|
521
|
+
method: "OPTIONS",
|
|
522
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
523
|
+
});
|
|
524
|
+
http.route({
|
|
525
|
+
path: "/proxy/brave_search",
|
|
526
|
+
method: "OPTIONS",
|
|
527
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
528
|
+
});
|
|
529
|
+
http.route({
|
|
530
|
+
path: "/proxy/resend",
|
|
531
|
+
method: "OPTIONS",
|
|
532
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
533
|
+
});
|
|
534
|
+
http.route({
|
|
535
|
+
path: "/proxy/elevenlabs",
|
|
536
|
+
method: "OPTIONS",
|
|
537
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
538
|
+
});
|
|
539
|
+
// 46elks SMS proxy
|
|
540
|
+
http.route({
|
|
541
|
+
path: "/proxy/46elks",
|
|
542
|
+
method: "POST",
|
|
543
|
+
handler: httpAction(async (ctx, request) => {
|
|
544
|
+
// Validate session and log usage
|
|
545
|
+
await validateAndLogProxyCall(ctx, request, "46elks", "send_sms");
|
|
546
|
+
const ELKS_USER = process.env.ELKS_API_USER;
|
|
547
|
+
const ELKS_PASS = process.env.ELKS_API_PASSWORD;
|
|
548
|
+
if (!ELKS_USER || !ELKS_PASS) {
|
|
549
|
+
return jsonResponse({ error: "46elks not configured" }, 500);
|
|
550
|
+
}
|
|
551
|
+
try {
|
|
552
|
+
const body = await request.json();
|
|
553
|
+
const { to, message, from = "APIClaw" } = body;
|
|
554
|
+
const auth = btoa(`${ELKS_USER}:${ELKS_PASS}`);
|
|
555
|
+
const response = await fetch("https://api.46elks.com/a1/sms", {
|
|
556
|
+
method: "POST",
|
|
557
|
+
headers: {
|
|
558
|
+
"Authorization": `Basic ${auth}`,
|
|
559
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
560
|
+
},
|
|
561
|
+
body: new URLSearchParams({ from, to, message }),
|
|
562
|
+
});
|
|
563
|
+
const data = await response.json();
|
|
564
|
+
return jsonResponse(data, response.status);
|
|
565
|
+
}
|
|
566
|
+
catch (e) {
|
|
567
|
+
return jsonResponse({ error: e.message }, 500);
|
|
568
|
+
}
|
|
569
|
+
}),
|
|
570
|
+
});
|
|
571
|
+
// Twilio SMS proxy
|
|
572
|
+
http.route({
|
|
573
|
+
path: "/proxy/twilio",
|
|
574
|
+
method: "POST",
|
|
575
|
+
handler: httpAction(async (ctx, request) => {
|
|
576
|
+
// Validate session and log usage
|
|
577
|
+
await validateAndLogProxyCall(ctx, request, "twilio", "send_sms");
|
|
578
|
+
const TWILIO_SID = process.env.TWILIO_ACCOUNT_SID;
|
|
579
|
+
const TWILIO_TOKEN = process.env.TWILIO_AUTH_TOKEN;
|
|
580
|
+
if (!TWILIO_SID || !TWILIO_TOKEN) {
|
|
581
|
+
return jsonResponse({ error: "Twilio not configured" }, 500);
|
|
582
|
+
}
|
|
583
|
+
try {
|
|
584
|
+
const body = await request.json();
|
|
585
|
+
const { to, message, from } = body;
|
|
586
|
+
if (!from) {
|
|
587
|
+
return jsonResponse({ error: "Twilio requires 'from' number" }, 400);
|
|
588
|
+
}
|
|
589
|
+
const auth = btoa(`${TWILIO_SID}:${TWILIO_TOKEN}`);
|
|
590
|
+
const response = await fetch(`https://api.twilio.com/2010-04-01/Accounts/${TWILIO_SID}/Messages.json`, {
|
|
591
|
+
method: "POST",
|
|
592
|
+
headers: {
|
|
593
|
+
"Authorization": `Basic ${auth}`,
|
|
594
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
595
|
+
},
|
|
596
|
+
body: new URLSearchParams({ To: to, From: from, Body: message }),
|
|
597
|
+
});
|
|
598
|
+
const data = await response.json();
|
|
599
|
+
return jsonResponse(data, response.status);
|
|
600
|
+
}
|
|
601
|
+
catch (e) {
|
|
602
|
+
return jsonResponse({ error: e.message }, 500);
|
|
603
|
+
}
|
|
604
|
+
}),
|
|
605
|
+
});
|
|
606
|
+
// CORS for new endpoints
|
|
607
|
+
http.route({
|
|
608
|
+
path: "/proxy/46elks",
|
|
609
|
+
method: "OPTIONS",
|
|
610
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
611
|
+
});
|
|
612
|
+
http.route({
|
|
613
|
+
path: "/proxy/twilio",
|
|
614
|
+
method: "OPTIONS",
|
|
615
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
616
|
+
});
|
|
617
|
+
// GitHub API proxy
|
|
618
|
+
http.route({
|
|
619
|
+
path: "/proxy/github",
|
|
620
|
+
method: "POST",
|
|
621
|
+
handler: httpAction(async (ctx, request) => {
|
|
622
|
+
// Validate session and log usage
|
|
623
|
+
const body = await request.json();
|
|
624
|
+
const action = body.action || "search_repos";
|
|
625
|
+
await validateAndLogProxyCall(ctx, request, "github", action);
|
|
626
|
+
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
|
|
627
|
+
if (!GITHUB_TOKEN) {
|
|
628
|
+
return jsonResponse({ error: "GitHub not configured" }, 500);
|
|
629
|
+
}
|
|
630
|
+
try {
|
|
631
|
+
const { action, ...params } = body;
|
|
632
|
+
let url;
|
|
633
|
+
let method = "GET";
|
|
634
|
+
let fetchBody;
|
|
635
|
+
// Route based on action
|
|
636
|
+
switch (action) {
|
|
637
|
+
case "search_repos":
|
|
638
|
+
const { query, sort = "stars", limit = 10 } = params;
|
|
639
|
+
url = `https://api.github.com/search/repositories?q=${encodeURIComponent(query)}&sort=${sort}&per_page=${limit}`;
|
|
640
|
+
break;
|
|
641
|
+
case "get_repo":
|
|
642
|
+
const { owner, repo } = params;
|
|
643
|
+
url = `https://api.github.com/repos/${owner}/${repo}`;
|
|
644
|
+
break;
|
|
645
|
+
case "list_issues":
|
|
646
|
+
const { owner: issueOwner, repo: issueRepo, state = "open", limit: issueLimit = 10 } = params;
|
|
647
|
+
url = `https://api.github.com/repos/${issueOwner}/${issueRepo}/issues?state=${state}&per_page=${issueLimit}`;
|
|
648
|
+
break;
|
|
649
|
+
case "create_issue":
|
|
650
|
+
const { owner: createOwner, repo: createRepo, title, body: issueBody = "" } = params;
|
|
651
|
+
url = `https://api.github.com/repos/${createOwner}/${createRepo}/issues`;
|
|
652
|
+
method = "POST";
|
|
653
|
+
fetchBody = JSON.stringify({ title, body: issueBody });
|
|
654
|
+
break;
|
|
655
|
+
case "get_file":
|
|
656
|
+
const { owner: fileOwner, repo: fileRepo, path } = params;
|
|
657
|
+
url = `https://api.github.com/repos/${fileOwner}/${fileRepo}/contents/${path}`;
|
|
658
|
+
break;
|
|
659
|
+
default:
|
|
660
|
+
return jsonResponse({ error: `Unknown action: ${action}` }, 400);
|
|
661
|
+
}
|
|
662
|
+
const response = await fetch(url, {
|
|
663
|
+
method,
|
|
664
|
+
headers: {
|
|
665
|
+
"Authorization": `Bearer ${GITHUB_TOKEN}`,
|
|
666
|
+
"Accept": "application/vnd.github+json",
|
|
667
|
+
"User-Agent": "APIClaw",
|
|
668
|
+
...(fetchBody ? { "Content-Type": "application/json" } : {}),
|
|
669
|
+
},
|
|
670
|
+
...(fetchBody ? { body: fetchBody } : {}),
|
|
671
|
+
});
|
|
672
|
+
const data = await response.json();
|
|
673
|
+
return jsonResponse(data, response.status);
|
|
674
|
+
}
|
|
675
|
+
catch (e) {
|
|
676
|
+
return jsonResponse({ error: e.message }, 500);
|
|
677
|
+
}
|
|
678
|
+
}),
|
|
679
|
+
});
|
|
680
|
+
http.route({
|
|
681
|
+
path: "/proxy/github",
|
|
682
|
+
method: "OPTIONS",
|
|
683
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
684
|
+
});
|
|
685
|
+
// ==============================================
|
|
686
|
+
// SERPER (Google Search) PROXY
|
|
687
|
+
// ==============================================
|
|
688
|
+
http.route({
|
|
689
|
+
path: "/proxy/serper",
|
|
690
|
+
method: "POST",
|
|
691
|
+
handler: httpAction(async (ctx, request) => {
|
|
692
|
+
await validateAndLogProxyCall(ctx, request, "serper", "search");
|
|
693
|
+
const SERPER_KEY = process.env.SERPER_API_KEY;
|
|
694
|
+
if (!SERPER_KEY) {
|
|
695
|
+
return jsonResponse({ error: "Serper not configured" }, 500);
|
|
696
|
+
}
|
|
697
|
+
try {
|
|
698
|
+
const body = await request.json();
|
|
699
|
+
const { query, q, num = 10, gl = "us", hl = "en" } = body;
|
|
700
|
+
const searchQuery = query || q;
|
|
701
|
+
if (!searchQuery) {
|
|
702
|
+
return jsonResponse({ error: "query required" }, 400);
|
|
703
|
+
}
|
|
704
|
+
const response = await fetch("https://google.serper.dev/search", {
|
|
705
|
+
method: "POST",
|
|
706
|
+
headers: {
|
|
707
|
+
"X-API-KEY": SERPER_KEY,
|
|
708
|
+
"Content-Type": "application/json",
|
|
709
|
+
},
|
|
710
|
+
body: JSON.stringify({ q: searchQuery, num, gl, hl }),
|
|
711
|
+
});
|
|
712
|
+
const data = await response.json();
|
|
713
|
+
return jsonResponse(data, response.status);
|
|
714
|
+
}
|
|
715
|
+
catch (e) {
|
|
716
|
+
return jsonResponse({ error: e.message }, 500);
|
|
717
|
+
}
|
|
718
|
+
}),
|
|
719
|
+
});
|
|
720
|
+
http.route({
|
|
721
|
+
path: "/proxy/serper",
|
|
722
|
+
method: "OPTIONS",
|
|
723
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
724
|
+
});
|
|
725
|
+
// ==============================================
|
|
726
|
+
// FIRECRAWL (Web Scraping) PROXY
|
|
727
|
+
// ==============================================
|
|
728
|
+
http.route({
|
|
729
|
+
path: "/proxy/firecrawl",
|
|
730
|
+
method: "POST",
|
|
731
|
+
handler: httpAction(async (ctx, request) => {
|
|
732
|
+
await validateAndLogProxyCall(ctx, request, "firecrawl", "scrape");
|
|
733
|
+
const FIRECRAWL_KEY = process.env.FIRECRAWL_API_KEY;
|
|
734
|
+
if (!FIRECRAWL_KEY) {
|
|
735
|
+
return jsonResponse({ error: "Firecrawl not configured" }, 500);
|
|
736
|
+
}
|
|
737
|
+
try {
|
|
738
|
+
const body = await request.json();
|
|
739
|
+
const { url, formats = ["markdown"], onlyMainContent = true } = body;
|
|
740
|
+
if (!url) {
|
|
741
|
+
return jsonResponse({ error: "url required" }, 400);
|
|
742
|
+
}
|
|
743
|
+
const response = await fetch("https://api.firecrawl.dev/v1/scrape", {
|
|
744
|
+
method: "POST",
|
|
745
|
+
headers: {
|
|
746
|
+
Authorization: `Bearer ${FIRECRAWL_KEY}`,
|
|
747
|
+
"Content-Type": "application/json",
|
|
748
|
+
},
|
|
749
|
+
body: JSON.stringify({ url, formats, onlyMainContent }),
|
|
750
|
+
});
|
|
751
|
+
const data = await response.json();
|
|
752
|
+
return jsonResponse(data, response.status);
|
|
753
|
+
}
|
|
754
|
+
catch (e) {
|
|
755
|
+
return jsonResponse({ error: e.message }, 500);
|
|
756
|
+
}
|
|
757
|
+
}),
|
|
758
|
+
});
|
|
759
|
+
http.route({
|
|
760
|
+
path: "/proxy/firecrawl",
|
|
761
|
+
method: "OPTIONS",
|
|
762
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
763
|
+
});
|
|
764
|
+
// ==============================================
|
|
765
|
+
// GROQ (LLM) PROXY
|
|
766
|
+
// ==============================================
|
|
767
|
+
http.route({
|
|
768
|
+
path: "/proxy/groq",
|
|
769
|
+
method: "POST",
|
|
770
|
+
handler: httpAction(async (ctx, request) => {
|
|
771
|
+
await validateAndLogProxyCall(ctx, request, "groq", "chat");
|
|
772
|
+
const GROQ_KEY = process.env.GROQ_API_KEY;
|
|
773
|
+
if (!GROQ_KEY) {
|
|
774
|
+
return jsonResponse({ error: "Groq not configured" }, 500);
|
|
775
|
+
}
|
|
776
|
+
try {
|
|
777
|
+
const body = await request.json();
|
|
778
|
+
const { model = "llama-3.3-70b-versatile", messages, temperature = 0.7, max_tokens = 1024 } = body;
|
|
779
|
+
if (!messages) {
|
|
780
|
+
return jsonResponse({ error: "messages required" }, 400);
|
|
781
|
+
}
|
|
782
|
+
const response = await fetch("https://api.groq.com/openai/v1/chat/completions", {
|
|
783
|
+
method: "POST",
|
|
784
|
+
headers: {
|
|
785
|
+
Authorization: `Bearer ${GROQ_KEY}`,
|
|
786
|
+
"Content-Type": "application/json",
|
|
787
|
+
},
|
|
788
|
+
body: JSON.stringify({ model, messages, temperature, max_tokens }),
|
|
789
|
+
});
|
|
790
|
+
const data = await response.json();
|
|
791
|
+
return jsonResponse(data, response.status);
|
|
792
|
+
}
|
|
793
|
+
catch (e) {
|
|
794
|
+
return jsonResponse({ error: e.message }, 500);
|
|
795
|
+
}
|
|
796
|
+
}),
|
|
797
|
+
});
|
|
798
|
+
http.route({
|
|
799
|
+
path: "/proxy/groq",
|
|
800
|
+
method: "OPTIONS",
|
|
801
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
802
|
+
});
|
|
803
|
+
// ==============================================
|
|
804
|
+
// MISTRAL (LLM/Embeddings) PROXY
|
|
805
|
+
// ==============================================
|
|
806
|
+
http.route({
|
|
807
|
+
path: "/proxy/mistral",
|
|
808
|
+
method: "POST",
|
|
809
|
+
handler: httpAction(async (ctx, request) => {
|
|
810
|
+
await validateAndLogProxyCall(ctx, request, "mistral", "chat");
|
|
811
|
+
const MISTRAL_KEY = process.env.MISTRAL_API_KEY;
|
|
812
|
+
if (!MISTRAL_KEY) {
|
|
813
|
+
return jsonResponse({ error: "Mistral not configured" }, 500);
|
|
814
|
+
}
|
|
815
|
+
try {
|
|
816
|
+
const body = await request.json();
|
|
817
|
+
const { model = "mistral-small-latest", messages, temperature = 0.7, max_tokens = 1024 } = body;
|
|
818
|
+
if (!messages) {
|
|
819
|
+
return jsonResponse({ error: "messages required" }, 400);
|
|
820
|
+
}
|
|
821
|
+
const response = await fetch("https://api.mistral.ai/v1/chat/completions", {
|
|
822
|
+
method: "POST",
|
|
823
|
+
headers: {
|
|
824
|
+
Authorization: `Bearer ${MISTRAL_KEY}`,
|
|
825
|
+
"Content-Type": "application/json",
|
|
826
|
+
},
|
|
827
|
+
body: JSON.stringify({ model, messages, temperature, max_tokens }),
|
|
828
|
+
});
|
|
829
|
+
const data = await response.json();
|
|
830
|
+
return jsonResponse(data, response.status);
|
|
831
|
+
}
|
|
832
|
+
catch (e) {
|
|
833
|
+
return jsonResponse({ error: e.message }, 500);
|
|
834
|
+
}
|
|
835
|
+
}),
|
|
836
|
+
});
|
|
837
|
+
http.route({
|
|
838
|
+
path: "/proxy/mistral",
|
|
839
|
+
method: "OPTIONS",
|
|
840
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
841
|
+
});
|
|
842
|
+
// ==============================================
|
|
843
|
+
// COHERE (LLM/Rerank) PROXY
|
|
844
|
+
// ==============================================
|
|
845
|
+
http.route({
|
|
846
|
+
path: "/proxy/cohere",
|
|
847
|
+
method: "POST",
|
|
848
|
+
handler: httpAction(async (ctx, request) => {
|
|
849
|
+
await validateAndLogProxyCall(ctx, request, "cohere", "chat");
|
|
850
|
+
const COHERE_KEY = process.env.COHERE_API_KEY;
|
|
851
|
+
if (!COHERE_KEY) {
|
|
852
|
+
return jsonResponse({ error: "Cohere not configured" }, 500);
|
|
853
|
+
}
|
|
854
|
+
try {
|
|
855
|
+
const body = await request.json();
|
|
856
|
+
const { model = "command-a-03-2025", message, chat_history, temperature = 0.7, max_tokens = 1024 } = body;
|
|
857
|
+
if (!message) {
|
|
858
|
+
return jsonResponse({ error: "message required" }, 400);
|
|
859
|
+
}
|
|
860
|
+
const response = await fetch("https://api.cohere.com/v2/chat", {
|
|
861
|
+
method: "POST",
|
|
862
|
+
headers: {
|
|
863
|
+
Authorization: `Bearer ${COHERE_KEY}`,
|
|
864
|
+
"Content-Type": "application/json",
|
|
865
|
+
},
|
|
866
|
+
body: JSON.stringify({ model, message, chat_history, temperature, max_tokens }),
|
|
867
|
+
});
|
|
868
|
+
const data = await response.json();
|
|
869
|
+
return jsonResponse(data, response.status);
|
|
870
|
+
}
|
|
871
|
+
catch (e) {
|
|
872
|
+
return jsonResponse({ error: e.message }, 500);
|
|
873
|
+
}
|
|
874
|
+
}),
|
|
875
|
+
});
|
|
876
|
+
http.route({
|
|
877
|
+
path: "/proxy/cohere",
|
|
878
|
+
method: "OPTIONS",
|
|
879
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
880
|
+
});
|
|
881
|
+
// ==============================================
|
|
882
|
+
// REPLICATE (ML Models) PROXY
|
|
883
|
+
// ==============================================
|
|
884
|
+
http.route({
|
|
885
|
+
path: "/proxy/replicate",
|
|
886
|
+
method: "POST",
|
|
887
|
+
handler: httpAction(async (ctx, request) => {
|
|
888
|
+
await validateAndLogProxyCall(ctx, request, "replicate", "prediction");
|
|
889
|
+
const REPLICATE_KEY = process.env.REPLICATE_API_TOKEN;
|
|
890
|
+
if (!REPLICATE_KEY) {
|
|
891
|
+
return jsonResponse({ error: "Replicate not configured" }, 500);
|
|
892
|
+
}
|
|
893
|
+
try {
|
|
894
|
+
const body = await request.json();
|
|
895
|
+
const { model, input, version } = body;
|
|
896
|
+
if (!model && !version) {
|
|
897
|
+
return jsonResponse({ error: "model or version required" }, 400);
|
|
898
|
+
}
|
|
899
|
+
const endpoint = version
|
|
900
|
+
? "https://api.replicate.com/v1/predictions"
|
|
901
|
+
: `https://api.replicate.com/v1/models/${model}/predictions`;
|
|
902
|
+
const payload = version ? { version, input } : { input };
|
|
903
|
+
const response = await fetch(endpoint, {
|
|
904
|
+
method: "POST",
|
|
905
|
+
headers: {
|
|
906
|
+
Authorization: `Bearer ${REPLICATE_KEY}`,
|
|
907
|
+
"Content-Type": "application/json",
|
|
908
|
+
Prefer: "wait",
|
|
909
|
+
},
|
|
910
|
+
body: JSON.stringify(payload),
|
|
911
|
+
});
|
|
912
|
+
const data = await response.json();
|
|
913
|
+
return jsonResponse(data, response.status);
|
|
914
|
+
}
|
|
915
|
+
catch (e) {
|
|
916
|
+
return jsonResponse({ error: e.message }, 500);
|
|
917
|
+
}
|
|
918
|
+
}),
|
|
919
|
+
});
|
|
920
|
+
http.route({
|
|
921
|
+
path: "/proxy/replicate",
|
|
922
|
+
method: "OPTIONS",
|
|
923
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
924
|
+
});
|
|
925
|
+
// ==============================================
|
|
926
|
+
// DEEPGRAM (Speech-to-Text) PROXY
|
|
927
|
+
// ==============================================
|
|
928
|
+
http.route({
|
|
929
|
+
path: "/proxy/deepgram",
|
|
930
|
+
method: "POST",
|
|
931
|
+
handler: httpAction(async (ctx, request) => {
|
|
932
|
+
await validateAndLogProxyCall(ctx, request, "deepgram", "transcribe");
|
|
933
|
+
const DEEPGRAM_KEY = process.env.DEEPGRAM_API_KEY;
|
|
934
|
+
if (!DEEPGRAM_KEY) {
|
|
935
|
+
return jsonResponse({ error: "Deepgram not configured" }, 500);
|
|
936
|
+
}
|
|
937
|
+
try {
|
|
938
|
+
const body = await request.json();
|
|
939
|
+
const { url, model = "nova-3", language = "en", smart_format = true } = body;
|
|
940
|
+
if (!url) {
|
|
941
|
+
return jsonResponse({ error: "url required (audio file URL)" }, 400);
|
|
942
|
+
}
|
|
943
|
+
const params = new URLSearchParams({
|
|
944
|
+
model,
|
|
945
|
+
language,
|
|
946
|
+
smart_format: String(smart_format),
|
|
947
|
+
});
|
|
948
|
+
const response = await fetch(`https://api.deepgram.com/v1/listen?${params}`, {
|
|
949
|
+
method: "POST",
|
|
950
|
+
headers: {
|
|
951
|
+
Authorization: `Token ${DEEPGRAM_KEY}`,
|
|
952
|
+
"Content-Type": "application/json",
|
|
953
|
+
},
|
|
954
|
+
body: JSON.stringify({ url }),
|
|
955
|
+
});
|
|
956
|
+
const data = await response.json();
|
|
957
|
+
return jsonResponse(data, response.status);
|
|
958
|
+
}
|
|
959
|
+
catch (e) {
|
|
960
|
+
return jsonResponse({ error: e.message }, 500);
|
|
961
|
+
}
|
|
962
|
+
}),
|
|
963
|
+
});
|
|
964
|
+
http.route({
|
|
965
|
+
path: "/proxy/deepgram",
|
|
966
|
+
method: "OPTIONS",
|
|
967
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
968
|
+
});
|
|
969
|
+
// ==============================================
|
|
970
|
+
// E2B (Code Sandbox) PROXY
|
|
971
|
+
// ==============================================
|
|
972
|
+
http.route({
|
|
973
|
+
path: "/proxy/e2b",
|
|
974
|
+
method: "POST",
|
|
975
|
+
handler: httpAction(async (ctx, request) => {
|
|
976
|
+
await validateAndLogProxyCall(ctx, request, "e2b", "execute");
|
|
977
|
+
const E2B_KEY = process.env.E2B_API_KEY;
|
|
978
|
+
if (!E2B_KEY) {
|
|
979
|
+
return jsonResponse({ error: "E2B not configured" }, 500);
|
|
980
|
+
}
|
|
981
|
+
try {
|
|
982
|
+
const body = await request.json();
|
|
983
|
+
const { code, language = "python", template = "base" } = body;
|
|
984
|
+
if (!code) {
|
|
985
|
+
return jsonResponse({ error: "code required" }, 400);
|
|
986
|
+
}
|
|
987
|
+
const response = await fetch("https://api.e2b.dev/sandboxes", {
|
|
988
|
+
method: "POST",
|
|
989
|
+
headers: {
|
|
990
|
+
"X-API-Key": E2B_KEY,
|
|
991
|
+
"Content-Type": "application/json",
|
|
992
|
+
},
|
|
993
|
+
body: JSON.stringify({ templateID: template, metadata: { language } }),
|
|
994
|
+
});
|
|
995
|
+
const sandbox = await response.json();
|
|
996
|
+
if (!response.ok) {
|
|
997
|
+
return jsonResponse(sandbox, response.status);
|
|
998
|
+
}
|
|
999
|
+
const execResponse = await fetch(`https://api.e2b.dev/sandboxes/${sandbox.sandboxID}/code/execution`, {
|
|
1000
|
+
method: "POST",
|
|
1001
|
+
headers: {
|
|
1002
|
+
"X-API-Key": E2B_KEY,
|
|
1003
|
+
"Content-Type": "application/json",
|
|
1004
|
+
},
|
|
1005
|
+
body: JSON.stringify({ code, language }),
|
|
1006
|
+
});
|
|
1007
|
+
const result = await execResponse.json();
|
|
1008
|
+
return jsonResponse(result, execResponse.status);
|
|
1009
|
+
}
|
|
1010
|
+
catch (e) {
|
|
1011
|
+
return jsonResponse({ error: e.message }, 500);
|
|
1012
|
+
}
|
|
1013
|
+
}),
|
|
1014
|
+
});
|
|
1015
|
+
http.route({
|
|
1016
|
+
path: "/proxy/e2b",
|
|
1017
|
+
method: "OPTIONS",
|
|
1018
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
1019
|
+
});
|
|
1020
|
+
// ==============================================
|
|
1021
|
+
// TOGETHER AI (Open-source LLM Inference) PROXY
|
|
1022
|
+
// ==============================================
|
|
1023
|
+
http.route({
|
|
1024
|
+
path: "/proxy/together",
|
|
1025
|
+
method: "POST",
|
|
1026
|
+
handler: httpAction(async (ctx, request) => {
|
|
1027
|
+
await validateAndLogProxyCall(ctx, request, "together", "chat");
|
|
1028
|
+
const TOGETHER_KEY = process.env.TOGETHER_API_KEY;
|
|
1029
|
+
if (!TOGETHER_KEY) {
|
|
1030
|
+
return jsonResponse({ error: "Together AI not configured" }, 500);
|
|
1031
|
+
}
|
|
1032
|
+
try {
|
|
1033
|
+
const body = await request.json();
|
|
1034
|
+
const { model = "meta-llama/Llama-3.3-70B-Instruct-Turbo", messages, temperature = 0.7, max_tokens = 1024 } = body;
|
|
1035
|
+
if (!messages || !Array.isArray(messages)) {
|
|
1036
|
+
return jsonResponse({ error: "messages array required" }, 400);
|
|
1037
|
+
}
|
|
1038
|
+
const response = await fetch("https://api.together.xyz/v1/chat/completions", {
|
|
1039
|
+
method: "POST",
|
|
1040
|
+
headers: {
|
|
1041
|
+
Authorization: `Bearer ${TOGETHER_KEY}`,
|
|
1042
|
+
"Content-Type": "application/json",
|
|
1043
|
+
},
|
|
1044
|
+
body: JSON.stringify({ model, messages, temperature, max_tokens }),
|
|
1045
|
+
});
|
|
1046
|
+
const data = await response.json();
|
|
1047
|
+
return jsonResponse(data, response.status);
|
|
1048
|
+
}
|
|
1049
|
+
catch (e) {
|
|
1050
|
+
return jsonResponse({ error: e.message }, 500);
|
|
1051
|
+
}
|
|
1052
|
+
}),
|
|
1053
|
+
});
|
|
1054
|
+
http.route({
|
|
1055
|
+
path: "/proxy/together",
|
|
1056
|
+
method: "OPTIONS",
|
|
1057
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
1058
|
+
});
|
|
1059
|
+
// ==============================================
|
|
1060
|
+
// STABILITY AI (Image Generation) PROXY
|
|
1061
|
+
// ==============================================
|
|
1062
|
+
http.route({
|
|
1063
|
+
path: "/proxy/stability",
|
|
1064
|
+
method: "POST",
|
|
1065
|
+
handler: httpAction(async (ctx, request) => {
|
|
1066
|
+
await validateAndLogProxyCall(ctx, request, "stability", "generate");
|
|
1067
|
+
const STABILITY_KEY = process.env.STABILITY_API_KEY;
|
|
1068
|
+
if (!STABILITY_KEY) {
|
|
1069
|
+
return jsonResponse({ error: "Stability AI not configured" }, 500);
|
|
1070
|
+
}
|
|
1071
|
+
try {
|
|
1072
|
+
const body = await request.json();
|
|
1073
|
+
const { prompt, model = "sd3.5-large", output_format = "png", aspect_ratio = "1:1" } = body;
|
|
1074
|
+
if (!prompt) {
|
|
1075
|
+
return jsonResponse({ error: "prompt required" }, 400);
|
|
1076
|
+
}
|
|
1077
|
+
const formData = new FormData();
|
|
1078
|
+
formData.append("prompt", prompt);
|
|
1079
|
+
formData.append("output_format", output_format);
|
|
1080
|
+
formData.append("aspect_ratio", aspect_ratio);
|
|
1081
|
+
const response = await fetch(`https://api.stability.ai/v2beta/stable-image/generate/${model}`, {
|
|
1082
|
+
method: "POST",
|
|
1083
|
+
headers: {
|
|
1084
|
+
Authorization: `Bearer ${STABILITY_KEY}`,
|
|
1085
|
+
Accept: "application/json",
|
|
1086
|
+
},
|
|
1087
|
+
body: formData,
|
|
1088
|
+
});
|
|
1089
|
+
const data = await response.json();
|
|
1090
|
+
return jsonResponse(data, response.status);
|
|
1091
|
+
}
|
|
1092
|
+
catch (e) {
|
|
1093
|
+
return jsonResponse({ error: e.message }, 500);
|
|
1094
|
+
}
|
|
1095
|
+
}),
|
|
1096
|
+
});
|
|
1097
|
+
http.route({
|
|
1098
|
+
path: "/proxy/stability",
|
|
1099
|
+
method: "OPTIONS",
|
|
1100
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
1101
|
+
});
|
|
1102
|
+
// ==============================================
|
|
1103
|
+
// ASSEMBLYAI (Audio Intelligence) PROXY
|
|
1104
|
+
// ==============================================
|
|
1105
|
+
http.route({
|
|
1106
|
+
path: "/proxy/assemblyai",
|
|
1107
|
+
method: "POST",
|
|
1108
|
+
handler: httpAction(async (ctx, request) => {
|
|
1109
|
+
await validateAndLogProxyCall(ctx, request, "assemblyai", "transcribe");
|
|
1110
|
+
const ASSEMBLYAI_KEY = process.env.ASSEMBLYAI_API_KEY;
|
|
1111
|
+
if (!ASSEMBLYAI_KEY) {
|
|
1112
|
+
return jsonResponse({ error: "AssemblyAI not configured" }, 500);
|
|
1113
|
+
}
|
|
1114
|
+
try {
|
|
1115
|
+
const body = await request.json();
|
|
1116
|
+
const { audio_url, language_detection = true, speaker_labels = true } = body;
|
|
1117
|
+
if (!audio_url) {
|
|
1118
|
+
return jsonResponse({ error: "audio_url required" }, 400);
|
|
1119
|
+
}
|
|
1120
|
+
const response = await fetch("https://api.assemblyai.com/v2/transcript", {
|
|
1121
|
+
method: "POST",
|
|
1122
|
+
headers: {
|
|
1123
|
+
Authorization: ASSEMBLYAI_KEY,
|
|
1124
|
+
"Content-Type": "application/json",
|
|
1125
|
+
},
|
|
1126
|
+
body: JSON.stringify({ audio_url, language_detection, speaker_labels }),
|
|
1127
|
+
});
|
|
1128
|
+
const data = await response.json();
|
|
1129
|
+
return jsonResponse(data, response.status);
|
|
1130
|
+
}
|
|
1131
|
+
catch (e) {
|
|
1132
|
+
return jsonResponse({ error: e.message }, 500);
|
|
1133
|
+
}
|
|
1134
|
+
}),
|
|
1135
|
+
});
|
|
1136
|
+
http.route({
|
|
1137
|
+
path: "/proxy/assemblyai",
|
|
1138
|
+
method: "OPTIONS",
|
|
1139
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
1140
|
+
});
|
|
1141
|
+
// ==============================================
|
|
1142
|
+
// APILAYER (Multi-API: Exchange, Stocks, Aviation, etc.) PROXY
|
|
1143
|
+
// ==============================================
|
|
1144
|
+
http.route({
|
|
1145
|
+
path: "/proxy/apilayer",
|
|
1146
|
+
method: "POST",
|
|
1147
|
+
handler: httpAction(async (ctx, request) => {
|
|
1148
|
+
await validateAndLogProxyCall(ctx, request, "apilayer", "call");
|
|
1149
|
+
const APILAYER_KEY = process.env.APILAYER_API_KEY;
|
|
1150
|
+
if (!APILAYER_KEY) {
|
|
1151
|
+
return jsonResponse({ error: "APILayer not configured" }, 500);
|
|
1152
|
+
}
|
|
1153
|
+
try {
|
|
1154
|
+
const body = await request.json();
|
|
1155
|
+
const { service, endpoint, params = {} } = body;
|
|
1156
|
+
if (!service || !endpoint) {
|
|
1157
|
+
return jsonResponse({ error: "service and endpoint required (e.g. service:'exchangerates', endpoint:'/latest')" }, 400);
|
|
1158
|
+
}
|
|
1159
|
+
const queryString = new URLSearchParams(params).toString();
|
|
1160
|
+
const url = `https://api.apilayer.com/${service}${endpoint}${queryString ? '?' + queryString : ''}`;
|
|
1161
|
+
const response = await fetch(url, {
|
|
1162
|
+
method: "GET",
|
|
1163
|
+
headers: {
|
|
1164
|
+
apikey: APILAYER_KEY,
|
|
1165
|
+
},
|
|
1166
|
+
});
|
|
1167
|
+
const data = await response.json();
|
|
1168
|
+
return jsonResponse(data, response.status);
|
|
1169
|
+
}
|
|
1170
|
+
catch (e) {
|
|
1171
|
+
return jsonResponse({ error: e.message }, 500);
|
|
1172
|
+
}
|
|
1173
|
+
}),
|
|
1174
|
+
});
|
|
1175
|
+
http.route({
|
|
1176
|
+
path: "/proxy/apilayer",
|
|
1177
|
+
method: "OPTIONS",
|
|
1178
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
1179
|
+
});
|
|
1180
|
+
// ==============================================
|
|
1181
|
+
// WORKSPACE / MAGIC LINK ENDPOINTS
|
|
1182
|
+
// ==============================================
|
|
1183
|
+
// Create magic link and send email
|
|
1184
|
+
http.route({
|
|
1185
|
+
path: "/workspace/magic-link",
|
|
1186
|
+
method: "POST",
|
|
1187
|
+
handler: httpAction(async (ctx, request) => {
|
|
1188
|
+
try {
|
|
1189
|
+
const body = await request.json();
|
|
1190
|
+
const { email, fingerprint } = body;
|
|
1191
|
+
if (!email || !email.includes("@")) {
|
|
1192
|
+
return jsonResponse({ error: "Valid email required" }, 400);
|
|
1193
|
+
}
|
|
1194
|
+
// Create magic link
|
|
1195
|
+
const result = await ctx.runMutation(api.workspaces.createMagicLink, {
|
|
1196
|
+
email: email.toLowerCase(),
|
|
1197
|
+
fingerprint,
|
|
1198
|
+
});
|
|
1199
|
+
// Send email directly - SIMPLE HTML (complex tables get stripped by Gmail)
|
|
1200
|
+
const verifyUrl = `https://apiclaw.nordsym.com/auth/verify?token=${result.token}`;
|
|
1201
|
+
const html = `<div style="font-family:Arial,sans-serif;max-width:500px;margin:0 auto;padding:20px;">
|
|
1202
|
+
<h1>🦞 APIClaw</h1>
|
|
1203
|
+
<h2>An AI Agent Wants to Connect</h2>
|
|
1204
|
+
<p>Click below to verify your email and activate your workspace.</p>
|
|
1205
|
+
<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>
|
|
1206
|
+
<p style="color:#666;font-size:13px;">Free tier: 50 API calls. This link expires in 1 hour.</p>
|
|
1207
|
+
<p style="color:#999;font-size:11px;">Or copy this link: ${verifyUrl}</p>
|
|
1208
|
+
</div>`;
|
|
1209
|
+
const RESEND_KEY = process.env.RESEND_API_KEY;
|
|
1210
|
+
if (!RESEND_KEY) {
|
|
1211
|
+
console.error("RESEND_API_KEY not configured");
|
|
1212
|
+
return jsonResponse({ error: "Email service not configured" }, 500);
|
|
1213
|
+
}
|
|
1214
|
+
const emailResponse = await fetch("https://api.resend.com/emails", {
|
|
1215
|
+
method: "POST",
|
|
1216
|
+
headers: {
|
|
1217
|
+
"Authorization": `Bearer ${RESEND_KEY}`,
|
|
1218
|
+
"Content-Type": "application/json",
|
|
1219
|
+
},
|
|
1220
|
+
body: JSON.stringify({
|
|
1221
|
+
from: "APIClaw <noreply@apiclaw.nordsym.com>",
|
|
1222
|
+
to: email.toLowerCase(),
|
|
1223
|
+
subject: "🦞 Verify Your Email — APIClaw",
|
|
1224
|
+
html: html,
|
|
1225
|
+
}),
|
|
1226
|
+
});
|
|
1227
|
+
if (!emailResponse.ok) {
|
|
1228
|
+
const errorText = await emailResponse.text();
|
|
1229
|
+
console.error("Resend error:", emailResponse.status, errorText);
|
|
1230
|
+
return jsonResponse({ error: "Failed to send email", details: errorText }, 500);
|
|
1231
|
+
}
|
|
1232
|
+
const emailResult = await emailResponse.json();
|
|
1233
|
+
console.log("Email sent successfully:", emailResult.id);
|
|
1234
|
+
return jsonResponse({
|
|
1235
|
+
success: true,
|
|
1236
|
+
token: result.token,
|
|
1237
|
+
expiresAt: result.expiresAt,
|
|
1238
|
+
message: "Magic link sent! Check your email.",
|
|
1239
|
+
emailId: emailResult.id,
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
catch (e) {
|
|
1243
|
+
console.error("Magic link error:", e);
|
|
1244
|
+
return jsonResponse({ error: e.message || "Failed to create magic link" }, 500);
|
|
1245
|
+
}
|
|
1246
|
+
}),
|
|
1247
|
+
});
|
|
1248
|
+
http.route({
|
|
1249
|
+
path: "/workspace/magic-link",
|
|
1250
|
+
method: "OPTIONS",
|
|
1251
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
1252
|
+
});
|
|
1253
|
+
// Poll magic link status (for agents to check if user clicked)
|
|
1254
|
+
http.route({
|
|
1255
|
+
path: "/workspace/poll",
|
|
1256
|
+
method: "GET",
|
|
1257
|
+
handler: httpAction(async (ctx, request) => {
|
|
1258
|
+
const url = new URL(request.url);
|
|
1259
|
+
const token = url.searchParams.get("token");
|
|
1260
|
+
if (!token) {
|
|
1261
|
+
return jsonResponse({ error: "token required" }, 400);
|
|
1262
|
+
}
|
|
1263
|
+
const result = await ctx.runQuery(api.workspaces.pollMagicLink, { token });
|
|
1264
|
+
return jsonResponse(result);
|
|
1265
|
+
}),
|
|
1266
|
+
});
|
|
1267
|
+
http.route({
|
|
1268
|
+
path: "/workspace/poll",
|
|
1269
|
+
method: "OPTIONS",
|
|
1270
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
1271
|
+
});
|
|
1272
|
+
// Verify session token
|
|
1273
|
+
http.route({
|
|
1274
|
+
path: "/workspace/verify-session",
|
|
1275
|
+
method: "GET",
|
|
1276
|
+
handler: httpAction(async (ctx, request) => {
|
|
1277
|
+
const url = new URL(request.url);
|
|
1278
|
+
const sessionToken = url.searchParams.get("sessionToken");
|
|
1279
|
+
if (!sessionToken) {
|
|
1280
|
+
return jsonResponse({ error: "sessionToken required" }, 400);
|
|
1281
|
+
}
|
|
1282
|
+
const result = await ctx.runQuery(api.workspaces.verifySession, { sessionToken });
|
|
1283
|
+
if (!result) {
|
|
1284
|
+
return jsonResponse({ error: "Invalid or expired session" }, 401);
|
|
1285
|
+
}
|
|
1286
|
+
return jsonResponse(result);
|
|
1287
|
+
}),
|
|
1288
|
+
});
|
|
1289
|
+
http.route({
|
|
1290
|
+
path: "/workspace/verify-session",
|
|
1291
|
+
method: "OPTIONS",
|
|
1292
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
1293
|
+
});
|
|
1294
|
+
// Get workspace by email
|
|
1295
|
+
http.route({
|
|
1296
|
+
path: "/workspace/by-email",
|
|
1297
|
+
method: "GET",
|
|
1298
|
+
handler: httpAction(async (ctx, request) => {
|
|
1299
|
+
const url = new URL(request.url);
|
|
1300
|
+
const email = url.searchParams.get("email");
|
|
1301
|
+
if (!email) {
|
|
1302
|
+
return jsonResponse({ error: "email required" }, 400);
|
|
1303
|
+
}
|
|
1304
|
+
const result = await ctx.runQuery(api.workspaces.getByEmail, { email });
|
|
1305
|
+
if (!result) {
|
|
1306
|
+
return jsonResponse({ exists: false });
|
|
1307
|
+
}
|
|
1308
|
+
return jsonResponse({ exists: true, workspace: result });
|
|
1309
|
+
}),
|
|
1310
|
+
});
|
|
1311
|
+
http.route({
|
|
1312
|
+
path: "/workspace/by-email",
|
|
1313
|
+
method: "OPTIONS",
|
|
1314
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
1315
|
+
});
|
|
1316
|
+
// Send reminder email
|
|
1317
|
+
http.route({
|
|
1318
|
+
path: "/workspace/send-reminder",
|
|
1319
|
+
method: "POST",
|
|
1320
|
+
handler: httpAction(async (ctx, request) => {
|
|
1321
|
+
try {
|
|
1322
|
+
const body = await request.json();
|
|
1323
|
+
const { email, token } = body;
|
|
1324
|
+
if (!email || !token) {
|
|
1325
|
+
return jsonResponse({ error: "email and token required" }, 400);
|
|
1326
|
+
}
|
|
1327
|
+
await ctx.runAction(api.email.sendReminderEmail, { email, token });
|
|
1328
|
+
return jsonResponse({ success: true });
|
|
1329
|
+
}
|
|
1330
|
+
catch (e) {
|
|
1331
|
+
return jsonResponse({ error: e.message }, 500);
|
|
1332
|
+
}
|
|
1333
|
+
}),
|
|
1334
|
+
});
|
|
1335
|
+
http.route({
|
|
1336
|
+
path: "/workspace/send-reminder",
|
|
1337
|
+
method: "OPTIONS",
|
|
1338
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
1339
|
+
});
|
|
1340
|
+
// ==============================================
|
|
1341
|
+
// STRIPE BILLING ENDPOINTS
|
|
1342
|
+
// ==============================================
|
|
1343
|
+
// Create checkout session
|
|
1344
|
+
http.route({
|
|
1345
|
+
path: "/api/billing/checkout",
|
|
1346
|
+
method: "POST",
|
|
1347
|
+
handler: createCheckoutSession,
|
|
1348
|
+
});
|
|
1349
|
+
http.route({
|
|
1350
|
+
path: "/api/billing/checkout",
|
|
1351
|
+
method: "OPTIONS",
|
|
1352
|
+
handler: checkoutOptions,
|
|
1353
|
+
});
|
|
1354
|
+
// Create billing portal session
|
|
1355
|
+
http.route({
|
|
1356
|
+
path: "/api/billing/portal",
|
|
1357
|
+
method: "POST",
|
|
1358
|
+
handler: createPortalSession,
|
|
1359
|
+
});
|
|
1360
|
+
http.route({
|
|
1361
|
+
path: "/api/billing/portal",
|
|
1362
|
+
method: "OPTIONS",
|
|
1363
|
+
handler: portalOptions,
|
|
1364
|
+
});
|
|
1365
|
+
// Stripe webhook handler
|
|
1366
|
+
http.route({
|
|
1367
|
+
path: "/api/webhooks/stripe",
|
|
1368
|
+
method: "POST",
|
|
1369
|
+
handler: handleStripeWebhook,
|
|
1370
|
+
});
|
|
1371
|
+
http.route({
|
|
1372
|
+
path: "/api/webhooks/stripe",
|
|
1373
|
+
method: "OPTIONS",
|
|
1374
|
+
handler: webhookOptions,
|
|
1375
|
+
});
|
|
1376
|
+
// Test endpoint to debug logging
|
|
1377
|
+
http.route({
|
|
1378
|
+
path: "/proxy/test-logging",
|
|
1379
|
+
method: "POST",
|
|
1380
|
+
handler: httpAction(async (ctx, request) => {
|
|
1381
|
+
const identifier = request.headers.get("X-APIClaw-Identifier");
|
|
1382
|
+
try {
|
|
1383
|
+
const logId = await ctx.runMutation(api.analytics.log, {
|
|
1384
|
+
event: "test_endpoint",
|
|
1385
|
+
provider: "test",
|
|
1386
|
+
identifier: identifier || "test",
|
|
1387
|
+
metadata: { test: true },
|
|
1388
|
+
});
|
|
1389
|
+
return jsonResponse({
|
|
1390
|
+
success: true,
|
|
1391
|
+
identifier,
|
|
1392
|
+
logId,
|
|
1393
|
+
message: "Logged successfully"
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
catch (e) {
|
|
1397
|
+
return jsonResponse({
|
|
1398
|
+
success: false,
|
|
1399
|
+
error: e.message,
|
|
1400
|
+
stack: e.stack
|
|
1401
|
+
}, 500);
|
|
1402
|
+
}
|
|
1403
|
+
}),
|
|
1404
|
+
});
|
|
1405
|
+
//# sourceMappingURL=http.js.map
|