@nordsym/apiclaw 1.2.2 → 1.2.3
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/AGENTS.md +50 -33
- package/README.md +22 -12
- package/SOUL.md +60 -19
- package/STATUS.md +91 -169
- package/convex/_generated/api.d.ts +6 -0
- package/convex/directCall.ts +598 -0
- package/convex/providers.ts +341 -26
- package/convex/schema.ts +87 -0
- package/convex/usage.ts +260 -0
- package/convex/waitlist.ts +55 -0
- package/data/combined-02-26.json +22102 -0
- package/data/night-expansion-02-26-06-batch2.json +1898 -0
- package/data/night-expansion-02-26-06-batch3.json +1410 -0
- package/data/night-expansion-02-26-06.json +3146 -0
- package/data/night-expansion-02-26-full.json +9726 -0
- package/data/night-expansion-02-26-v2.json +330 -0
- package/data/night-expansion-02-26.json +171 -0
- package/dist/crypto.d.ts +7 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +67 -0
- package/dist/crypto.js.map +1 -0
- package/dist/execute-dynamic.d.ts +116 -0
- package/dist/execute-dynamic.d.ts.map +1 -0
- package/dist/execute-dynamic.js +456 -0
- package/dist/execute-dynamic.js.map +1 -0
- package/dist/execute.d.ts +2 -1
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +35 -5
- package/dist/execute.js.map +1 -1
- package/dist/index.js +33 -4
- package/dist/index.js.map +1 -1
- package/dist/registry/apis.json +2081 -3
- package/docs/PRD-customer-key-passthrough.md +184 -0
- package/landing/public/badges/available-on-apiclaw.svg +14 -0
- package/landing/scripts/generate-stats.js +75 -4
- package/landing/src/app/admin/page.tsx +1 -1
- package/landing/src/app/api/auth/magic-link/route.ts +1 -1
- package/landing/src/app/api/auth/session/route.ts +1 -1
- package/landing/src/app/api/auth/verify/route.ts +1 -1
- package/landing/src/app/api/og/route.tsx +5 -3
- package/landing/src/app/docs/page.tsx +5 -4
- package/landing/src/app/earn/page.tsx +14 -11
- package/landing/src/app/globals.css +16 -15
- package/landing/src/app/layout.tsx +2 -2
- package/landing/src/app/page.tsx +425 -254
- package/landing/src/app/providers/dashboard/[apiId]/actions/[actionId]/edit/page.tsx +600 -0
- package/landing/src/app/providers/dashboard/[apiId]/actions/new/page.tsx +583 -0
- package/landing/src/app/providers/dashboard/[apiId]/actions/page.tsx +301 -0
- package/landing/src/app/providers/dashboard/[apiId]/direct-call/page.tsx +659 -0
- package/landing/src/app/providers/dashboard/[apiId]/page.tsx +381 -0
- package/landing/src/app/providers/dashboard/[apiId]/test/page.tsx +418 -0
- package/landing/src/app/providers/dashboard/layout.tsx +292 -0
- package/landing/src/app/providers/dashboard/page.tsx +353 -290
- package/landing/src/app/providers/register/page.tsx +87 -10
- package/landing/src/components/AiClientDropdown.tsx +85 -0
- package/landing/src/components/ConfigHelperModal.tsx +113 -0
- package/landing/src/components/HeroTabs.tsx +187 -0
- package/landing/src/components/ShareIntegrationModal.tsx +198 -0
- package/landing/src/hooks/useDashboardData.ts +53 -1
- package/landing/src/lib/apis.json +46554 -174
- package/landing/src/lib/convex-client.ts +22 -3
- package/landing/src/lib/stats.json +4 -4
- package/landing/tsconfig.tsbuildinfo +1 -1
- package/night-expansion-02-26-06-batch2.py +368 -0
- package/night-expansion-02-26-06-batch3.py +299 -0
- package/night-expansion-02-26-06.py +756 -0
- package/package.json +1 -1
- package/scripts/bulk-add-public-apis-v2.py +418 -0
- package/scripts/night-expansion-02-26-v2.py +296 -0
- package/scripts/night-expansion-02-26.py +890 -0
- package/scripts/seed-complete-api.js +181 -0
- package/scripts/seed-demo-api.sh +44 -0
- package/src/crypto.ts +75 -0
- package/src/execute-dynamic.ts +589 -0
- package/src/execute.ts +41 -5
- package/src/index.ts +38 -4
- package/src/registry/apis.json +2081 -3
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { useParams, useRouter } from "next/navigation";
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
import {
|
|
7
|
+
Save,
|
|
8
|
+
Loader2,
|
|
9
|
+
AlertCircle,
|
|
10
|
+
Check,
|
|
11
|
+
Eye,
|
|
12
|
+
EyeOff,
|
|
13
|
+
Info,
|
|
14
|
+
PlayCircle,
|
|
15
|
+
ChevronLeft,
|
|
16
|
+
Shield,
|
|
17
|
+
Zap,
|
|
18
|
+
Clock,
|
|
19
|
+
DollarSign,
|
|
20
|
+
Globe,
|
|
21
|
+
Key,
|
|
22
|
+
ArrowRight,
|
|
23
|
+
} from "lucide-react";
|
|
24
|
+
import { convexQuery, convexMutation, type ProviderAPI } from "@/lib/convex-client";
|
|
25
|
+
import { ShareIntegrationModal } from "@/components/ShareIntegrationModal";
|
|
26
|
+
|
|
27
|
+
interface DirectCallConfig {
|
|
28
|
+
_id?: string;
|
|
29
|
+
apiId: string;
|
|
30
|
+
baseUrl: string;
|
|
31
|
+
authType: "bearer" | "basic" | "api_key" | "none";
|
|
32
|
+
authHeader: string;
|
|
33
|
+
authPrefix: string;
|
|
34
|
+
masterApiKey: string;
|
|
35
|
+
rateLimitPerUser: number;
|
|
36
|
+
rateLimitPerDay: number;
|
|
37
|
+
pricePerRequest: number;
|
|
38
|
+
status: "draft" | "testing" | "live";
|
|
39
|
+
// Customer key passthrough
|
|
40
|
+
allowCustomerKeys: boolean;
|
|
41
|
+
requireCustomerKeys: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const authTypes = [
|
|
45
|
+
{ value: "bearer", label: "Bearer Token", description: "Authorization: Bearer <token>" },
|
|
46
|
+
{ value: "basic", label: "Basic Auth", description: "Authorization: Basic <base64>" },
|
|
47
|
+
{ value: "api_key", label: "API Key Header", description: "Custom header with API key" },
|
|
48
|
+
{ value: "none", label: "No Auth", description: "Public API, no authentication" },
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
export default function DirectCallSetupPage() {
|
|
52
|
+
const params = useParams();
|
|
53
|
+
const router = useRouter();
|
|
54
|
+
const apiId = params.apiId as string;
|
|
55
|
+
|
|
56
|
+
const [api, setApi] = useState<ProviderAPI | null>(null);
|
|
57
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
58
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
59
|
+
const [error, setError] = useState<string | null>(null);
|
|
60
|
+
const [success, setSuccess] = useState<string | null>(null);
|
|
61
|
+
const [showApiKey, setShowApiKey] = useState(false);
|
|
62
|
+
const [urlError, setUrlError] = useState<string | null>(null);
|
|
63
|
+
const [showShareModal, setShowShareModal] = useState(false);
|
|
64
|
+
const [wasLiveBefore, setWasLiveBefore] = useState(false);
|
|
65
|
+
|
|
66
|
+
const [formData, setFormData] = useState<DirectCallConfig>({
|
|
67
|
+
apiId,
|
|
68
|
+
baseUrl: "",
|
|
69
|
+
authType: "bearer",
|
|
70
|
+
authHeader: "Authorization",
|
|
71
|
+
authPrefix: "Bearer",
|
|
72
|
+
masterApiKey: "",
|
|
73
|
+
rateLimitPerUser: 60,
|
|
74
|
+
rateLimitPerDay: 10000,
|
|
75
|
+
pricePerRequest: 1,
|
|
76
|
+
status: "draft",
|
|
77
|
+
allowCustomerKeys: true,
|
|
78
|
+
requireCustomerKeys: false,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
const loadData = async () => {
|
|
83
|
+
const token = localStorage.getItem("apiclaw_session");
|
|
84
|
+
if (!token) {
|
|
85
|
+
router.push("/providers/dashboard/login");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
// Load API details
|
|
91
|
+
const apiData = await convexQuery<ProviderAPI | null>("providers:getApiById", { apiId });
|
|
92
|
+
if (!apiData) {
|
|
93
|
+
setError("API not found");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
setApi(apiData);
|
|
97
|
+
|
|
98
|
+
// Load existing Direct Call config if exists
|
|
99
|
+
try {
|
|
100
|
+
const existingConfig = await convexQuery<DirectCallConfig | null>("directCall:getConfig", { apiId });
|
|
101
|
+
if (existingConfig) {
|
|
102
|
+
setFormData({
|
|
103
|
+
...existingConfig,
|
|
104
|
+
masterApiKey: "", // Never show existing key, placeholder only
|
|
105
|
+
});
|
|
106
|
+
// Track if already live (to not show modal on re-saves)
|
|
107
|
+
if (existingConfig.status === "live") {
|
|
108
|
+
setWasLiveBefore(true);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// No existing config - that's fine
|
|
113
|
+
}
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.error("Failed to load data:", err);
|
|
116
|
+
setError(err instanceof Error ? err.message : "Failed to load data");
|
|
117
|
+
} finally {
|
|
118
|
+
setIsLoading(false);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
loadData();
|
|
123
|
+
}, [apiId, router]);
|
|
124
|
+
|
|
125
|
+
const validateUrl = (url: string): boolean => {
|
|
126
|
+
if (!url) {
|
|
127
|
+
setUrlError("Base URL is required");
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
const parsed = new URL(url);
|
|
133
|
+
|
|
134
|
+
// Must be HTTPS
|
|
135
|
+
if (parsed.protocol !== "https:") {
|
|
136
|
+
setUrlError("URL must use HTTPS");
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Block private IP ranges
|
|
141
|
+
const blockedPatterns = [
|
|
142
|
+
/^127\./,
|
|
143
|
+
/^10\./,
|
|
144
|
+
/^192\.168\./,
|
|
145
|
+
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
|
|
146
|
+
/^localhost$/i,
|
|
147
|
+
/^0\.0\.0\.0$/,
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
if (blockedPatterns.some((p) => p.test(parsed.hostname))) {
|
|
151
|
+
setUrlError("Private/local URLs are not allowed");
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
setUrlError(null);
|
|
156
|
+
return true;
|
|
157
|
+
} catch {
|
|
158
|
+
setUrlError("Invalid URL format");
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const updateField = <K extends keyof DirectCallConfig>(
|
|
164
|
+
field: K,
|
|
165
|
+
value: DirectCallConfig[K]
|
|
166
|
+
) => {
|
|
167
|
+
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
168
|
+
setError(null);
|
|
169
|
+
setSuccess(null);
|
|
170
|
+
|
|
171
|
+
if (field === "baseUrl") {
|
|
172
|
+
validateUrl(value as string);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Update auth prefix based on auth type
|
|
176
|
+
if (field === "authType") {
|
|
177
|
+
const type = value as DirectCallConfig["authType"];
|
|
178
|
+
if (type === "bearer") {
|
|
179
|
+
setFormData((prev) => ({ ...prev, authHeader: "Authorization", authPrefix: "Bearer" }));
|
|
180
|
+
} else if (type === "basic") {
|
|
181
|
+
setFormData((prev) => ({ ...prev, authHeader: "Authorization", authPrefix: "Basic" }));
|
|
182
|
+
} else if (type === "api_key") {
|
|
183
|
+
setFormData((prev) => ({ ...prev, authHeader: "X-API-Key", authPrefix: "" }));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const handleSave = async () => {
|
|
189
|
+
// Validate URL
|
|
190
|
+
if (!validateUrl(formData.baseUrl)) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Validate required fields
|
|
195
|
+
if (formData.authType !== "none" && !formData.masterApiKey && !formData._id) {
|
|
196
|
+
setError("Master API key is required for authenticated APIs");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
setIsSaving(true);
|
|
201
|
+
setError(null);
|
|
202
|
+
setSuccess(null);
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const token = localStorage.getItem("apiclaw_session");
|
|
206
|
+
if (!token) {
|
|
207
|
+
router.push("/providers/dashboard/login");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
await convexMutation("directCall:saveConfig", {
|
|
212
|
+
token,
|
|
213
|
+
config: {
|
|
214
|
+
apiId,
|
|
215
|
+
baseUrl: formData.baseUrl,
|
|
216
|
+
authType: formData.authType,
|
|
217
|
+
authHeader: formData.authHeader,
|
|
218
|
+
authPrefix: formData.authPrefix,
|
|
219
|
+
masterApiKey: formData.masterApiKey || undefined, // Only send if changed
|
|
220
|
+
rateLimitPerUser: formData.rateLimitPerUser,
|
|
221
|
+
rateLimitPerDay: formData.rateLimitPerDay,
|
|
222
|
+
pricePerRequest: formData.pricePerRequest,
|
|
223
|
+
status: formData.status,
|
|
224
|
+
allowCustomerKeys: formData.allowCustomerKeys,
|
|
225
|
+
requireCustomerKeys: formData.requireCustomerKeys,
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
setSuccess("Configuration saved successfully!");
|
|
230
|
+
|
|
231
|
+
// Clear the API key field after save (it's been encrypted)
|
|
232
|
+
setFormData((prev) => ({ ...prev, masterApiKey: "" }));
|
|
233
|
+
|
|
234
|
+
// Show share modal when going live for the first time
|
|
235
|
+
if (formData.status === "live" && !wasLiveBefore) {
|
|
236
|
+
setShowShareModal(true);
|
|
237
|
+
setWasLiveBefore(true);
|
|
238
|
+
}
|
|
239
|
+
} catch (err) {
|
|
240
|
+
console.error("Save failed:", err);
|
|
241
|
+
setError(err instanceof Error ? err.message : "Failed to save configuration");
|
|
242
|
+
} finally {
|
|
243
|
+
setIsSaving(false);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const handleStatusChange = async (newStatus: "draft" | "testing" | "live") => {
|
|
248
|
+
// Validate before going live
|
|
249
|
+
if (newStatus === "live") {
|
|
250
|
+
if (!formData.baseUrl || urlError) {
|
|
251
|
+
setError("Valid base URL required before going live");
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (formData.authType !== "none" && !formData._id && !formData.masterApiKey) {
|
|
255
|
+
setError("API key must be saved before going live");
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
updateField("status", newStatus);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
if (isLoading) {
|
|
264
|
+
return (
|
|
265
|
+
<div className="flex items-center justify-center py-24">
|
|
266
|
+
<Loader2 className="w-8 h-8 text-accent animate-spin" />
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (error && !api) {
|
|
272
|
+
return (
|
|
273
|
+
<div className="text-center py-24">
|
|
274
|
+
<AlertCircle className="w-16 h-16 text-red-500 mx-auto mb-4" />
|
|
275
|
+
<h1 className="text-2xl font-bold mb-2">Error</h1>
|
|
276
|
+
<p className="text-text-muted mb-6">{error}</p>
|
|
277
|
+
<Link href="/providers/dashboard" className="btn-primary">
|
|
278
|
+
Back to Dashboard
|
|
279
|
+
</Link>
|
|
280
|
+
</div>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const getStatusBadge = (status: string, isActive: boolean) => {
|
|
285
|
+
const baseClasses = "flex items-center gap-2 px-4 py-3 rounded-xl border cursor-pointer transition-all";
|
|
286
|
+
|
|
287
|
+
switch (status) {
|
|
288
|
+
case "live":
|
|
289
|
+
return `${baseClasses} ${
|
|
290
|
+
isActive
|
|
291
|
+
? "border-green-500 bg-green-500/20 text-green-500"
|
|
292
|
+
: "border-border hover:border-green-500/50"
|
|
293
|
+
}`;
|
|
294
|
+
case "testing":
|
|
295
|
+
return `${baseClasses} ${
|
|
296
|
+
isActive
|
|
297
|
+
? "border-yellow-500 bg-yellow-500/20 text-yellow-600"
|
|
298
|
+
: "border-border hover:border-yellow-500/50"
|
|
299
|
+
}`;
|
|
300
|
+
default:
|
|
301
|
+
return `${baseClasses} ${
|
|
302
|
+
isActive
|
|
303
|
+
? "border-gray-500 bg-gray-500/20 text-gray-400"
|
|
304
|
+
: "border-border hover:border-gray-500/50"
|
|
305
|
+
}`;
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
return (
|
|
310
|
+
<div className="max-w-3xl space-y-8">
|
|
311
|
+
{/* Header */}
|
|
312
|
+
<div className="flex items-center gap-4">
|
|
313
|
+
<Link
|
|
314
|
+
href={`/providers/dashboard/${apiId}`}
|
|
315
|
+
className="p-2 rounded-lg hover:bg-surface transition"
|
|
316
|
+
>
|
|
317
|
+
<ChevronLeft className="w-5 h-5" />
|
|
318
|
+
</Link>
|
|
319
|
+
<div>
|
|
320
|
+
<h1 className="text-2xl font-bold">Direct Call Setup</h1>
|
|
321
|
+
<p className="text-text-muted">{api?.name}</p>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
{/* Status Selector */}
|
|
326
|
+
<div className="rounded-2xl border border-border bg-surface-elevated p-6">
|
|
327
|
+
<h2 className="font-semibold mb-4">Status</h2>
|
|
328
|
+
<div className="grid grid-cols-3 gap-3">
|
|
329
|
+
<button
|
|
330
|
+
onClick={() => handleStatusChange("draft")}
|
|
331
|
+
className={getStatusBadge("draft", formData.status === "draft")}
|
|
332
|
+
>
|
|
333
|
+
<Clock className="w-5 h-5" />
|
|
334
|
+
<div className="text-left">
|
|
335
|
+
<p className="font-medium">Draft</p>
|
|
336
|
+
<p className="text-xs text-text-muted">Not active</p>
|
|
337
|
+
</div>
|
|
338
|
+
</button>
|
|
339
|
+
<button
|
|
340
|
+
onClick={() => handleStatusChange("testing")}
|
|
341
|
+
className={getStatusBadge("testing", formData.status === "testing")}
|
|
342
|
+
>
|
|
343
|
+
<PlayCircle className="w-5 h-5" />
|
|
344
|
+
<div className="text-left">
|
|
345
|
+
<p className="font-medium">Testing</p>
|
|
346
|
+
<p className="text-xs text-text-muted">Test mode</p>
|
|
347
|
+
</div>
|
|
348
|
+
</button>
|
|
349
|
+
<button
|
|
350
|
+
onClick={() => handleStatusChange("live")}
|
|
351
|
+
className={getStatusBadge("live", formData.status === "live")}
|
|
352
|
+
>
|
|
353
|
+
<Zap className="w-5 h-5" />
|
|
354
|
+
<div className="text-left">
|
|
355
|
+
<p className="font-medium">Live</p>
|
|
356
|
+
<p className="text-xs text-text-muted">Production</p>
|
|
357
|
+
</div>
|
|
358
|
+
</button>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
|
|
362
|
+
{/* Base URL */}
|
|
363
|
+
<div className="rounded-2xl border border-border bg-surface-elevated p-6">
|
|
364
|
+
<div className="flex items-center gap-2 mb-4">
|
|
365
|
+
<Globe className="w-5 h-5 text-accent" />
|
|
366
|
+
<h2 className="font-semibold">Base URL</h2>
|
|
367
|
+
</div>
|
|
368
|
+
<div>
|
|
369
|
+
<input
|
|
370
|
+
type="url"
|
|
371
|
+
value={formData.baseUrl}
|
|
372
|
+
onChange={(e) => updateField("baseUrl", e.target.value)}
|
|
373
|
+
placeholder="https://api.example.com/v1"
|
|
374
|
+
className={`w-full px-4 py-3 rounded-xl bg-surface border focus:outline-none transition ${
|
|
375
|
+
urlError
|
|
376
|
+
? "border-red-500 focus:border-red-500"
|
|
377
|
+
: "border-border focus:border-accent"
|
|
378
|
+
}`}
|
|
379
|
+
/>
|
|
380
|
+
{urlError && (
|
|
381
|
+
<p className="mt-2 text-sm text-red-500 flex items-center gap-1">
|
|
382
|
+
<AlertCircle className="w-4 h-4" />
|
|
383
|
+
{urlError}
|
|
384
|
+
</p>
|
|
385
|
+
)}
|
|
386
|
+
<p className="mt-2 text-xs text-text-muted">
|
|
387
|
+
Must be HTTPS. This is the base URL for all API calls.
|
|
388
|
+
</p>
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
|
|
392
|
+
{/* Authentication */}
|
|
393
|
+
<div className="rounded-2xl border border-border bg-surface-elevated p-6">
|
|
394
|
+
<div className="flex items-center gap-2 mb-4">
|
|
395
|
+
<Shield className="w-5 h-5 text-accent" />
|
|
396
|
+
<h2 className="font-semibold">Authentication</h2>
|
|
397
|
+
</div>
|
|
398
|
+
|
|
399
|
+
{/* Auth Type */}
|
|
400
|
+
<div className="space-y-4">
|
|
401
|
+
<div>
|
|
402
|
+
<label className="block text-sm font-medium mb-2">Auth Type</label>
|
|
403
|
+
<div className="grid grid-cols-2 gap-3">
|
|
404
|
+
{authTypes.map((type) => (
|
|
405
|
+
<button
|
|
406
|
+
key={type.value}
|
|
407
|
+
onClick={() => updateField("authType", type.value as DirectCallConfig["authType"])}
|
|
408
|
+
className={`text-left p-3 rounded-xl border transition ${
|
|
409
|
+
formData.authType === type.value
|
|
410
|
+
? "border-accent bg-accent/10"
|
|
411
|
+
: "border-border hover:border-accent/50"
|
|
412
|
+
}`}
|
|
413
|
+
>
|
|
414
|
+
<p className="font-medium text-sm">{type.label}</p>
|
|
415
|
+
<p className="text-xs text-text-muted mt-0.5">{type.description}</p>
|
|
416
|
+
</button>
|
|
417
|
+
))}
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
|
|
421
|
+
{formData.authType !== "none" && (
|
|
422
|
+
<>
|
|
423
|
+
{/* Auth Header */}
|
|
424
|
+
<div>
|
|
425
|
+
<label className="block text-sm font-medium mb-2">Auth Header</label>
|
|
426
|
+
<input
|
|
427
|
+
type="text"
|
|
428
|
+
value={formData.authHeader}
|
|
429
|
+
onChange={(e) => updateField("authHeader", e.target.value)}
|
|
430
|
+
placeholder="Authorization"
|
|
431
|
+
className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition"
|
|
432
|
+
/>
|
|
433
|
+
<p className="mt-1 text-xs text-text-muted">
|
|
434
|
+
The HTTP header name for authentication
|
|
435
|
+
</p>
|
|
436
|
+
</div>
|
|
437
|
+
|
|
438
|
+
{/* Master API Key */}
|
|
439
|
+
<div>
|
|
440
|
+
<label className="block text-sm font-medium mb-2">Master API Key</label>
|
|
441
|
+
<div className="relative">
|
|
442
|
+
<Key className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-text-muted" />
|
|
443
|
+
<input
|
|
444
|
+
type={showApiKey ? "text" : "password"}
|
|
445
|
+
value={formData.masterApiKey}
|
|
446
|
+
onChange={(e) => updateField("masterApiKey", e.target.value)}
|
|
447
|
+
placeholder={formData._id ? "••••••••••••••••" : "Enter your API key"}
|
|
448
|
+
className="w-full pl-12 pr-12 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition"
|
|
449
|
+
/>
|
|
450
|
+
<button
|
|
451
|
+
type="button"
|
|
452
|
+
onClick={() => setShowApiKey(!showApiKey)}
|
|
453
|
+
className="absolute right-4 top-1/2 -translate-y-1/2 text-text-muted hover:text-text-primary transition"
|
|
454
|
+
>
|
|
455
|
+
{showApiKey ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
|
456
|
+
</button>
|
|
457
|
+
</div>
|
|
458
|
+
<div className="mt-2 flex items-start gap-2 text-xs text-text-muted">
|
|
459
|
+
<Info className="w-4 h-4 flex-shrink-0 mt-0.5" />
|
|
460
|
+
<span>
|
|
461
|
+
Your API key is encrypted with AES-256-GCM before storage.
|
|
462
|
+
{formData._id && " Leave blank to keep the existing key."}
|
|
463
|
+
</span>
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
</>
|
|
467
|
+
)}
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
|
|
471
|
+
{/* Rate Limits */}
|
|
472
|
+
<div className="rounded-2xl border border-border bg-surface-elevated p-6">
|
|
473
|
+
<div className="flex items-center gap-2 mb-4">
|
|
474
|
+
<Clock className="w-5 h-5 text-accent" />
|
|
475
|
+
<h2 className="font-semibold">Rate Limits</h2>
|
|
476
|
+
</div>
|
|
477
|
+
<div className="grid md:grid-cols-2 gap-4">
|
|
478
|
+
<div>
|
|
479
|
+
<label className="block text-sm font-medium mb-2">Per User (requests/min)</label>
|
|
480
|
+
<input
|
|
481
|
+
type="number"
|
|
482
|
+
min="1"
|
|
483
|
+
max="10000"
|
|
484
|
+
value={formData.rateLimitPerUser}
|
|
485
|
+
onChange={(e) => updateField("rateLimitPerUser", parseInt(e.target.value) || 60)}
|
|
486
|
+
className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition"
|
|
487
|
+
/>
|
|
488
|
+
<p className="mt-1 text-xs text-text-muted">
|
|
489
|
+
Max requests per minute per user
|
|
490
|
+
</p>
|
|
491
|
+
</div>
|
|
492
|
+
<div>
|
|
493
|
+
<label className="block text-sm font-medium mb-2">Per Day (requests/day)</label>
|
|
494
|
+
<input
|
|
495
|
+
type="number"
|
|
496
|
+
min="1"
|
|
497
|
+
max="1000000"
|
|
498
|
+
value={formData.rateLimitPerDay}
|
|
499
|
+
onChange={(e) => updateField("rateLimitPerDay", parseInt(e.target.value) || 10000)}
|
|
500
|
+
className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition"
|
|
501
|
+
/>
|
|
502
|
+
<p className="mt-1 text-xs text-text-muted">
|
|
503
|
+
Max requests per day per user
|
|
504
|
+
</p>
|
|
505
|
+
</div>
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
|
|
509
|
+
{/* Customer Authentication */}
|
|
510
|
+
<div className="rounded-2xl border border-border bg-surface-elevated p-6">
|
|
511
|
+
<div className="flex items-center gap-2 mb-4">
|
|
512
|
+
<Key className="w-5 h-5 text-accent" />
|
|
513
|
+
<h2 className="font-semibold">Customer Authentication</h2>
|
|
514
|
+
</div>
|
|
515
|
+
<p className="text-sm text-text-muted mb-4">
|
|
516
|
+
Control how agents authenticate with your API. By default, APIClaw uses your master key.
|
|
517
|
+
Enable customer keys to let agents pass their own API keys (useful for multi-tenant SaaS).
|
|
518
|
+
</p>
|
|
519
|
+
<div className="space-y-4">
|
|
520
|
+
<label className="flex items-start gap-3 cursor-pointer">
|
|
521
|
+
<input
|
|
522
|
+
type="checkbox"
|
|
523
|
+
checked={formData.allowCustomerKeys}
|
|
524
|
+
onChange={(e) => updateField("allowCustomerKeys", e.target.checked)}
|
|
525
|
+
className="mt-1 w-5 h-5 rounded border-border text-accent focus:ring-accent"
|
|
526
|
+
/>
|
|
527
|
+
<div>
|
|
528
|
+
<span className="font-medium">Allow customer keys</span>
|
|
529
|
+
<p className="text-sm text-text-muted">
|
|
530
|
+
Agents can optionally pass their own API key via <code className="text-xs bg-surface px-1 rounded">customer_key</code> parameter
|
|
531
|
+
</p>
|
|
532
|
+
</div>
|
|
533
|
+
</label>
|
|
534
|
+
<label className="flex items-start gap-3 cursor-pointer">
|
|
535
|
+
<input
|
|
536
|
+
type="checkbox"
|
|
537
|
+
checked={formData.requireCustomerKeys}
|
|
538
|
+
onChange={(e) => updateField("requireCustomerKeys", e.target.checked)}
|
|
539
|
+
className="mt-1 w-5 h-5 rounded border-border text-accent focus:ring-accent"
|
|
540
|
+
/>
|
|
541
|
+
<div>
|
|
542
|
+
<span className="font-medium">Require customer keys</span>
|
|
543
|
+
<p className="text-sm text-text-muted">
|
|
544
|
+
Agents <strong>must</strong> provide their own API key. Your master key will not be used as fallback.
|
|
545
|
+
<br />
|
|
546
|
+
<span className="text-accent">Recommended for multi-tenant SaaS where each customer has their own account.</span>
|
|
547
|
+
</p>
|
|
548
|
+
</div>
|
|
549
|
+
</label>
|
|
550
|
+
</div>
|
|
551
|
+
</div>
|
|
552
|
+
|
|
553
|
+
{/* Pricing */}
|
|
554
|
+
<div className="rounded-2xl border border-border bg-surface-elevated p-6">
|
|
555
|
+
<div className="flex items-center gap-2 mb-4">
|
|
556
|
+
<DollarSign className="w-5 h-5 text-accent" />
|
|
557
|
+
<h2 className="font-semibold">Pricing</h2>
|
|
558
|
+
</div>
|
|
559
|
+
<div>
|
|
560
|
+
<label className="block text-sm font-medium mb-2">Price per Request (credits)</label>
|
|
561
|
+
<input
|
|
562
|
+
type="number"
|
|
563
|
+
min="0"
|
|
564
|
+
step="0.1"
|
|
565
|
+
value={formData.pricePerRequest}
|
|
566
|
+
onChange={(e) => updateField("pricePerRequest", parseFloat(e.target.value) || 0)}
|
|
567
|
+
className="w-full px-4 py-3 rounded-xl bg-surface border border-border focus:border-accent focus:outline-none transition"
|
|
568
|
+
/>
|
|
569
|
+
<p className="mt-1 text-xs text-text-muted">
|
|
570
|
+
How many credits to charge per API call. Users buy credits from APIClaw.
|
|
571
|
+
</p>
|
|
572
|
+
</div>
|
|
573
|
+
</div>
|
|
574
|
+
|
|
575
|
+
{/* Error/Success Messages */}
|
|
576
|
+
{error && (
|
|
577
|
+
<div className="p-4 rounded-xl bg-red-500/10 border border-red-500/30 flex items-center gap-3 text-red-500">
|
|
578
|
+
<AlertCircle className="w-5 h-5 flex-shrink-0" />
|
|
579
|
+
<span className="text-sm">{error}</span>
|
|
580
|
+
</div>
|
|
581
|
+
)}
|
|
582
|
+
|
|
583
|
+
{success && (
|
|
584
|
+
<div className="p-4 rounded-xl bg-green-500/10 border border-green-500/30 flex items-center gap-3 text-green-500">
|
|
585
|
+
<Check className="w-5 h-5 flex-shrink-0" />
|
|
586
|
+
<span className="text-sm">{success}</span>
|
|
587
|
+
</div>
|
|
588
|
+
)}
|
|
589
|
+
|
|
590
|
+
{/* Save Button */}
|
|
591
|
+
<div className="flex items-center justify-between pt-4 border-t border-border">
|
|
592
|
+
<Link
|
|
593
|
+
href={`/providers/dashboard/${apiId}`}
|
|
594
|
+
className="btn-secondary !py-2.5"
|
|
595
|
+
>
|
|
596
|
+
Cancel
|
|
597
|
+
</Link>
|
|
598
|
+
<button
|
|
599
|
+
onClick={handleSave}
|
|
600
|
+
disabled={isSaving || !!urlError}
|
|
601
|
+
className="btn-primary disabled:opacity-50 disabled:cursor-not-allowed"
|
|
602
|
+
>
|
|
603
|
+
{isSaving ? (
|
|
604
|
+
<>
|
|
605
|
+
<Loader2 className="w-5 h-5 animate-spin" />
|
|
606
|
+
Saving...
|
|
607
|
+
</>
|
|
608
|
+
) : (
|
|
609
|
+
<>
|
|
610
|
+
<Save className="w-5 h-5" />
|
|
611
|
+
Save Configuration
|
|
612
|
+
</>
|
|
613
|
+
)}
|
|
614
|
+
</button>
|
|
615
|
+
</div>
|
|
616
|
+
|
|
617
|
+
{/* Next Steps */}
|
|
618
|
+
{formData._id && formData.status !== "live" && (
|
|
619
|
+
<div className="rounded-2xl border border-accent/30 bg-accent/5 p-6">
|
|
620
|
+
<h3 className="font-semibold mb-3">🚀 Next Steps</h3>
|
|
621
|
+
<ol className="space-y-3">
|
|
622
|
+
<li className="flex items-start gap-3 text-sm text-text-secondary">
|
|
623
|
+
<span className="w-6 h-6 rounded-full bg-accent/20 text-accent flex items-center justify-center flex-shrink-0 text-xs font-bold">1</span>
|
|
624
|
+
<span>
|
|
625
|
+
<Link href={`/providers/dashboard/${apiId}/actions`} className="text-accent hover:underline">
|
|
626
|
+
Define your Actions
|
|
627
|
+
</Link>
|
|
628
|
+
{" "}— Add the endpoints agents can call
|
|
629
|
+
</span>
|
|
630
|
+
</li>
|
|
631
|
+
<li className="flex items-start gap-3 text-sm text-text-secondary">
|
|
632
|
+
<span className="w-6 h-6 rounded-full bg-accent/20 text-accent flex items-center justify-center flex-shrink-0 text-xs font-bold">2</span>
|
|
633
|
+
<span>
|
|
634
|
+
<Link href={`/providers/dashboard/${apiId}/test`} className="text-accent hover:underline">
|
|
635
|
+
Test your Actions
|
|
636
|
+
</Link>
|
|
637
|
+
{" "}— Verify everything works before going live
|
|
638
|
+
</span>
|
|
639
|
+
</li>
|
|
640
|
+
<li className="flex items-start gap-3 text-sm text-text-secondary">
|
|
641
|
+
<span className="w-6 h-6 rounded-full bg-accent/20 text-accent flex items-center justify-center flex-shrink-0 text-xs font-bold">3</span>
|
|
642
|
+
<span>Set status to <strong>Live</strong> — Your API becomes available to all agents</span>
|
|
643
|
+
</li>
|
|
644
|
+
</ol>
|
|
645
|
+
</div>
|
|
646
|
+
)}
|
|
647
|
+
|
|
648
|
+
{/* Share Integration Modal */}
|
|
649
|
+
<ShareIntegrationModal
|
|
650
|
+
isOpen={showShareModal}
|
|
651
|
+
onClose={() => setShowShareModal(false)}
|
|
652
|
+
providerName={api?.name || ""}
|
|
653
|
+
apiName={api?.name || ""}
|
|
654
|
+
apiSlug={api?.name?.toLowerCase().replace(/\s+/g, "-") || apiId}
|
|
655
|
+
description={api?.description}
|
|
656
|
+
/>
|
|
657
|
+
</div>
|
|
658
|
+
);
|
|
659
|
+
}
|