@nordsym/apiclaw 1.5.9 → 1.5.11
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/APILAYER_STATUS_2026-03-24.md +38 -0
- package/CHANGELOG-WHITELIST-V2.md +269 -0
- package/HIVR-INTEGRATION.md +281 -0
- package/HIVR-WHITELIST-STATUS.md +205 -0
- package/HIVR-WHITELIST.md +148 -0
- package/TERMINOLOGY-AUDIT.md +99 -0
- package/TERMINOLOGY-FIXED.md +74 -0
- package/VIDEO-DEMO-GUIDE.md +82 -0
- package/WHITELIST-ARCHITECTURE.md +379 -0
- package/api/discover.ts +71 -0
- package/api/health.ts +20 -0
- package/convex/http.d.ts.map +1 -1
- package/convex/http.js +8 -0
- package/convex/http.js.map +1 -1
- package/convex/http.ts +8 -0
- package/direct-test.mjs +51 -0
- package/dist/access-control.d.ts +45 -0
- package/dist/access-control.d.ts.map +1 -0
- package/dist/access-control.js +142 -0
- package/dist/access-control.js.map +1 -0
- package/dist/analytics.d.ts +4 -0
- package/dist/analytics.d.ts.map +1 -1
- package/dist/analytics.js +1 -0
- package/dist/analytics.js.map +1 -1
- package/dist/cli/commands/mcp-install.d.ts.map +1 -1
- package/dist/cli/commands/mcp-install.js +55 -40
- package/dist/cli/commands/mcp-install.js.map +1 -1
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +148 -0
- package/dist/credentials.js.map +1 -1
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +191 -82
- package/dist/discovery.js.map +1 -1
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +274 -0
- package/dist/execute.js.map +1 -1
- package/dist/hivr-whitelist.d.ts +18 -0
- package/dist/hivr-whitelist.d.ts.map +1 -0
- package/dist/hivr-whitelist.js +95 -0
- package/dist/hivr-whitelist.js.map +1 -0
- package/dist/http-api.d.ts.map +1 -1
- package/dist/http-api.js +17 -33
- package/dist/http-api.js.map +1 -1
- package/dist/http-server-minimal.d.ts +7 -0
- package/dist/http-server-minimal.d.ts.map +1 -0
- package/dist/http-server-minimal.js +126 -0
- package/dist/http-server-minimal.js.map +1 -0
- package/dist/product-whitelist.d.ts +37 -0
- package/dist/product-whitelist.d.ts.map +1 -0
- package/dist/product-whitelist.js +203 -0
- package/dist/product-whitelist.js.map +1 -0
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +1 -1
- package/dist/proxy.js.map +1 -1
- package/email-templates/README.md +104 -0
- package/email-templates/partnership-template.html +116 -0
- package/landing/next-env.d.ts +1 -0
- package/landing/pages/api/discover.ts +43 -0
- package/landing/pages/api/health.ts +20 -0
- package/landing/src/app/api/auth/magic-link/route.ts +1 -1
- package/landing/src/app/auth/verify/page.tsx +6 -0
- package/landing/src/app/dashboard/verify/page.tsx +6 -0
- package/landing/src/app/join/page.tsx +6 -0
- package/landing/src/app/layout.tsx +2 -2
- package/landing/src/app/login/page.tsx +1 -1
- package/landing/src/app/mou/[partnerId]/page.tsx +6 -0
- package/landing/src/app/page.tsx +39 -18
- package/landing/src/app/providers/dashboard/[apiId]/actions/[actionId]/edit/page.tsx +6 -0
- package/landing/src/app/providers/dashboard/[apiId]/actions/new/page.tsx +5 -0
- package/landing/src/app/providers/dashboard/[apiId]/actions/page.tsx +5 -0
- package/landing/src/app/providers/dashboard/[apiId]/direct-call/page.tsx +6 -1
- package/landing/src/app/providers/dashboard/[apiId]/page.tsx +5 -0
- package/landing/src/app/providers/dashboard/[apiId]/test/page.tsx +5 -0
- package/landing/src/app/providers/dashboard/layout.tsx +6 -6
- package/landing/src/app/providers/dashboard/login/page.tsx +1 -1
- package/landing/src/app/providers/dashboard/page.tsx +1 -1
- package/landing/src/app/providers/dashboard/verify/page.tsx +6 -0
- package/landing/src/app/providers/layout.tsx +1 -1
- package/landing/src/app/upgrade/page.tsx +6 -0
- package/landing/src/app/workspace/page.tsx +6 -0
- package/landing/src/components/HeroTabs.tsx +2 -2
- package/landing/src/components/VideoDemo.tsx +94 -0
- package/landing/src/components/{ProviderDashboard.tsx → Workspace.tsx} +2 -2
- package/landing/src/lib/mock-data.ts +1 -1
- package/landing/src/lib/stats.json +1 -1
- package/package.json +4 -2
- package/scripts/test-whitelist-v2.sh +128 -0
- package/src/access-control.ts +174 -0
- package/src/analytics.ts +5 -0
- package/src/cli/commands/mcp-install.ts +14 -4
- package/src/credentials.ts +156 -0
- package/src/discovery.ts +191 -82
- package/src/execute.ts +274 -0
- package/src/hivr-whitelist.ts +110 -0
- package/src/http-api.ts +18 -34
- package/src/http-server-minimal.ts +154 -0
- package/src/product-whitelist.ts +246 -0
- package/src/proxy.ts +1 -1
- package/test-actual-handlers.ts +92 -0
- package/test-apilayer-all-14.ts +249 -0
- package/test-apilayer-fixed.ts +248 -0
- package/test-direct-endpoints.ts +174 -0
- package/test-exact-endpoints.ts +144 -0
- package/test-final.ts +83 -0
- package/test-full-routing.ts +100 -0
- package/test-handlers-correct.ts +217 -0
- package/test-numverify-key.ts +41 -0
- package/test-via-handlers.ts +92 -0
- package/test-worldnews.mjs +26 -0
|
@@ -80,12 +80,12 @@ export default function DashboardLayout({
|
|
|
80
80
|
};
|
|
81
81
|
|
|
82
82
|
// Check if current route is API detail page
|
|
83
|
-
const apiIdMatch = pathname
|
|
83
|
+
const apiIdMatch = pathname?.match(/\/providers\/dashboard\/([^/]+)/);
|
|
84
84
|
const currentApiId = apiIdMatch ? apiIdMatch[1] : null;
|
|
85
85
|
const isApiDetailPage = currentApiId && currentApiId !== "login" && currentApiId !== "verify";
|
|
86
86
|
|
|
87
87
|
// Don't show layout for login/verify pages
|
|
88
|
-
if (pathname
|
|
88
|
+
if (pathname?.includes("/login") || pathname?.includes("/verify")) {
|
|
89
89
|
return <>{children}</>;
|
|
90
90
|
}
|
|
91
91
|
|
|
@@ -167,7 +167,7 @@ export default function DashboardLayout({
|
|
|
167
167
|
key={item.label}
|
|
168
168
|
href={item.href}
|
|
169
169
|
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition ${
|
|
170
|
-
(item.exact ? pathname === item.href : pathname
|
|
170
|
+
(item.exact ? pathname === item.href : pathname?.startsWith(item.href.split('?')[0])) && !isApiDetailPage
|
|
171
171
|
? "bg-accent text-white"
|
|
172
172
|
: "text-text-secondary hover:bg-surface hover:text-text-primary"
|
|
173
173
|
}`}
|
|
@@ -200,7 +200,7 @@ export default function DashboardLayout({
|
|
|
200
200
|
<Link
|
|
201
201
|
href={`/providers/dashboard/${currentApiId}/direct-call`}
|
|
202
202
|
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition ${
|
|
203
|
-
pathname
|
|
203
|
+
pathname?.includes("/direct-call")
|
|
204
204
|
? "bg-accent text-white"
|
|
205
205
|
: "text-text-secondary hover:bg-surface hover:text-text-primary"
|
|
206
206
|
}`}
|
|
@@ -212,7 +212,7 @@ export default function DashboardLayout({
|
|
|
212
212
|
<Link
|
|
213
213
|
href={`/providers/dashboard/${currentApiId}/actions`}
|
|
214
214
|
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition ${
|
|
215
|
-
pathname
|
|
215
|
+
pathname?.includes("/actions")
|
|
216
216
|
? "bg-accent text-white"
|
|
217
217
|
: "text-text-secondary hover:bg-surface hover:text-text-primary"
|
|
218
218
|
}`}
|
|
@@ -224,7 +224,7 @@ export default function DashboardLayout({
|
|
|
224
224
|
<Link
|
|
225
225
|
href={`/providers/dashboard/${currentApiId}/test`}
|
|
226
226
|
className={`flex items-center gap-3 px-3 py-2 rounded-lg transition ${
|
|
227
|
-
pathname
|
|
227
|
+
pathname?.includes("/test")
|
|
228
228
|
? "bg-accent text-white"
|
|
229
229
|
: "text-text-secondary hover:bg-surface hover:text-text-primary"
|
|
230
230
|
}`}
|
|
@@ -109,7 +109,7 @@ export default function LoginPage() {
|
|
|
109
109
|
<div className="pt-32 pb-24 px-6">
|
|
110
110
|
<div className="max-w-md mx-auto">
|
|
111
111
|
<div className="text-center mb-8">
|
|
112
|
-
<h1 className="text-3xl font-bold mb-2">
|
|
112
|
+
<h1 className="text-3xl font-bold mb-2">Workspace</h1>
|
|
113
113
|
<p className="text-text-secondary">
|
|
114
114
|
Sign in to manage your APIs and view analytics
|
|
115
115
|
</p>
|
|
@@ -4,7 +4,7 @@ import { useEffect } from "react";
|
|
|
4
4
|
import { useRouter } from "next/navigation";
|
|
5
5
|
import { Loader2 } from "lucide-react";
|
|
6
6
|
|
|
7
|
-
export default function
|
|
7
|
+
export default function WorkspaceRedirect() {
|
|
8
8
|
const router = useRouter();
|
|
9
9
|
|
|
10
10
|
useEffect(() => {
|
|
@@ -8,6 +8,12 @@ import Link from "next/link";
|
|
|
8
8
|
function VerifyContent() {
|
|
9
9
|
const router = useRouter();
|
|
10
10
|
const searchParams = useSearchParams();
|
|
11
|
+
|
|
12
|
+
// Handle null searchParams
|
|
13
|
+
if (!searchParams) {
|
|
14
|
+
return <div className="min-h-screen flex items-center justify-center">Loading...</div>;
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
const token = searchParams.get("token");
|
|
12
18
|
|
|
13
19
|
const [status, setStatus] = useState<"verifying" | "success" | "error">("verifying");
|
|
@@ -40,6 +40,12 @@ async function actionConvex<T>(path: string, args: Record<string, unknown>): Pro
|
|
|
40
40
|
function UpgradeContent() {
|
|
41
41
|
const searchParams = useSearchParams();
|
|
42
42
|
const router = useRouter();
|
|
43
|
+
|
|
44
|
+
// Handle null searchParams
|
|
45
|
+
if (!searchParams) {
|
|
46
|
+
return <div className="min-h-screen flex items-center justify-center">Loading...</div>;
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
const workspaceId = searchParams.get("ws");
|
|
44
50
|
const success = searchParams.get("success");
|
|
45
51
|
|
|
@@ -184,6 +184,12 @@ function generatePreviewAnalytics(): ProviderAnalytics {
|
|
|
184
184
|
export default function WorkspacePage() {
|
|
185
185
|
const router = useRouter();
|
|
186
186
|
const searchParams = useSearchParams();
|
|
187
|
+
|
|
188
|
+
// Handle null searchParams
|
|
189
|
+
if (!searchParams) {
|
|
190
|
+
return <div className="min-h-screen flex items-center justify-center">Loading...</div>;
|
|
191
|
+
}
|
|
192
|
+
|
|
187
193
|
const tabFromUrl = searchParams.get("tab") as TabType | null;
|
|
188
194
|
const subFromUrl = searchParams.get("sub") as AnalyticsSubtab | null;
|
|
189
195
|
|
|
@@ -179,10 +179,10 @@ export function HeroTabs() {
|
|
|
179
179
|
</ul>
|
|
180
180
|
|
|
181
181
|
<a
|
|
182
|
-
href="/
|
|
182
|
+
href="/workspace"
|
|
183
183
|
className="btn-primary w-full justify-center"
|
|
184
184
|
>
|
|
185
|
-
Open
|
|
185
|
+
Open Workspace
|
|
186
186
|
<ArrowRight className="w-4 h-4" />
|
|
187
187
|
</a>
|
|
188
188
|
</div>
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Play, X } from "lucide-react";
|
|
5
|
+
|
|
6
|
+
interface VideoDemoProps {
|
|
7
|
+
videoUrl?: string; // Tella embed URL
|
|
8
|
+
thumbnailUrl?: string; // Optional custom thumbnail
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function VideoDemo({
|
|
12
|
+
videoUrl = "PASTE_TELLA_URL_HERE",
|
|
13
|
+
thumbnailUrl
|
|
14
|
+
}: VideoDemoProps) {
|
|
15
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
16
|
+
const isVideoReady = videoUrl !== "PASTE_TELLA_URL_HERE" && videoUrl.startsWith("http");
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
{/* Floating Video Button - Bottom Right */}
|
|
21
|
+
<button
|
|
22
|
+
onClick={() => setIsOpen(true)}
|
|
23
|
+
className="fixed bottom-8 right-8 z-40 group"
|
|
24
|
+
aria-label="Watch demo video"
|
|
25
|
+
>
|
|
26
|
+
<div className="relative">
|
|
27
|
+
{/* Pulse animation */}
|
|
28
|
+
<div className="absolute inset-0 bg-[#ef4444] rounded-full opacity-75 animate-ping" />
|
|
29
|
+
|
|
30
|
+
{/* Main button */}
|
|
31
|
+
<div className="relative bg-[#ef4444] hover:bg-[#dc2626] text-white rounded-full p-4 shadow-lg transition-all duration-200 flex items-center gap-3 pr-6">
|
|
32
|
+
<Play className="w-6 h-6 fill-white" />
|
|
33
|
+
<span className="font-medium text-sm whitespace-nowrap">
|
|
34
|
+
Watch Demo
|
|
35
|
+
</span>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</button>
|
|
39
|
+
|
|
40
|
+
{/* Video Modal */}
|
|
41
|
+
{isOpen && (
|
|
42
|
+
<div
|
|
43
|
+
className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4"
|
|
44
|
+
onClick={() => setIsOpen(false)}
|
|
45
|
+
>
|
|
46
|
+
<div
|
|
47
|
+
className="relative w-full max-w-4xl bg-[var(--background)] rounded-2xl shadow-2xl overflow-hidden"
|
|
48
|
+
onClick={(e) => e.stopPropagation()}
|
|
49
|
+
>
|
|
50
|
+
{/* Close button */}
|
|
51
|
+
<button
|
|
52
|
+
onClick={() => setIsOpen(false)}
|
|
53
|
+
className="absolute top-4 right-4 z-10 bg-black/50 hover:bg-black/70 text-white rounded-full p-2 transition-colors"
|
|
54
|
+
aria-label="Close video"
|
|
55
|
+
>
|
|
56
|
+
<X className="w-5 h-5" />
|
|
57
|
+
</button>
|
|
58
|
+
|
|
59
|
+
{/* Video container - 16:9 aspect ratio */}
|
|
60
|
+
<div className="relative w-full" style={{ paddingBottom: '56.25%' }}>
|
|
61
|
+
{isVideoReady ? (
|
|
62
|
+
<iframe
|
|
63
|
+
src={videoUrl}
|
|
64
|
+
className="absolute inset-0 w-full h-full"
|
|
65
|
+
frameBorder="0"
|
|
66
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
67
|
+
allowFullScreen
|
|
68
|
+
/>
|
|
69
|
+
) : (
|
|
70
|
+
<div className="absolute inset-0 flex items-center justify-center bg-gradient-to-br from-[#0f172a] to-[#1e293b]">
|
|
71
|
+
<div className="text-center">
|
|
72
|
+
<Play className="w-16 h-16 mx-auto mb-4 text-[#ef4444]" />
|
|
73
|
+
<h3 className="text-2xl font-bold text-white mb-2">Coming Soon</h3>
|
|
74
|
+
<p className="text-[#94a3b8]">Demo video will be available shortly</p>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
{/* Video info */}
|
|
81
|
+
<div className="p-6 border-t border-[var(--border)]">
|
|
82
|
+
<h3 className="text-lg font-semibold text-[var(--text)] mb-2">
|
|
83
|
+
🦞 APIClaw Quick Start
|
|
84
|
+
</h3>
|
|
85
|
+
<p className="text-sm text-[var(--text-muted)]">
|
|
86
|
+
Install and use APIClaw in under 2 minutes
|
|
87
|
+
</p>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
</>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -50,7 +50,7 @@ type TabType = "overview" | "apis" | "earnings";
|
|
|
50
50
|
|
|
51
51
|
const COLORS = ["#ef4444", "#f97316", "#eab308", "#22c55e", "#3b82f6"];
|
|
52
52
|
|
|
53
|
-
export function
|
|
53
|
+
export function Workspace() {
|
|
54
54
|
const [activeTab, setActiveTab] = useState<TabType>("overview");
|
|
55
55
|
const [period, setPeriod] = useState<"week" | "month" | "all">("month");
|
|
56
56
|
const [selectedApi, setSelectedApi] = useState<string | null>(null);
|
|
@@ -76,7 +76,7 @@ export function ProviderDashboard() {
|
|
|
76
76
|
🦞
|
|
77
77
|
</div>
|
|
78
78
|
<div>
|
|
79
|
-
<h1 className="font-bold text-lg">
|
|
79
|
+
<h1 className="font-bold text-lg">Workspace</h1>
|
|
80
80
|
<p className="text-sm text-text-muted">{provider.company || provider.email}</p>
|
|
81
81
|
</div>
|
|
82
82
|
</div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nordsym/apiclaw",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.11",
|
|
4
4
|
"description": "The API layer for AI agents. Dashboard + 22K APIs + 18 Direct Call providers. MCP native.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"build": "tsc && cp -r src/registry dist/",
|
|
17
17
|
"dev": "tsx watch src/index.ts",
|
|
18
18
|
"start": "node dist/index.js",
|
|
19
|
+
"start:http": "tsx src/http-server-minimal.ts",
|
|
19
20
|
"test": "tsx src/test.ts",
|
|
20
21
|
"test:e2e": "tsx src/test.ts",
|
|
21
22
|
"webhook": "tsx src/webhook.ts",
|
|
@@ -66,7 +67,7 @@
|
|
|
66
67
|
"@types/inquirer": "^9.0.9",
|
|
67
68
|
"chalk": "^5.6.2",
|
|
68
69
|
"commander": "^14.0.3",
|
|
69
|
-
"convex": "^1.
|
|
70
|
+
"convex": "^1.33.1",
|
|
70
71
|
"dotenv": "^17.3.1",
|
|
71
72
|
"fs-extra": "^11.3.3",
|
|
72
73
|
"hono": "^4.0.0",
|
|
@@ -77,6 +78,7 @@
|
|
|
77
78
|
"devDependencies": {
|
|
78
79
|
"@types/fs-extra": "^11.0.4",
|
|
79
80
|
"@types/node": "^20.10.0",
|
|
81
|
+
"@vercel/node": "^5.6.16",
|
|
80
82
|
"tsx": "^4.7.0",
|
|
81
83
|
"typescript": "^5.3.0"
|
|
82
84
|
},
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Test script for APIClaw Whitelist v2.0
|
|
3
|
+
# Tests whitelist, access control, and analytics
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
echo "🧪 APIClaw Whitelist v2.0 Test Suite"
|
|
8
|
+
echo "===================================="
|
|
9
|
+
echo ""
|
|
10
|
+
|
|
11
|
+
API_URL="${APICLAW_API_URL:-http://localhost:3000}"
|
|
12
|
+
|
|
13
|
+
# Colors
|
|
14
|
+
GREEN='\033[0;32m'
|
|
15
|
+
RED='\033[0;31m'
|
|
16
|
+
YELLOW='\033[1;33m'
|
|
17
|
+
NC='\033[0m' # No Color
|
|
18
|
+
|
|
19
|
+
function test_case() {
|
|
20
|
+
local name=$1
|
|
21
|
+
local expected=$2
|
|
22
|
+
local cmd=$3
|
|
23
|
+
|
|
24
|
+
echo -n "Testing: $name ... "
|
|
25
|
+
|
|
26
|
+
response=$(eval "$cmd" 2>&1)
|
|
27
|
+
status=$?
|
|
28
|
+
|
|
29
|
+
if echo "$response" | grep -q "$expected"; then
|
|
30
|
+
echo -e "${GREEN}✓ PASS${NC}"
|
|
31
|
+
return 0
|
|
32
|
+
else
|
|
33
|
+
echo -e "${RED}✗ FAIL${NC}"
|
|
34
|
+
echo " Expected: $expected"
|
|
35
|
+
echo " Got: $response"
|
|
36
|
+
return 1
|
|
37
|
+
fi
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
echo "1. Test Whitelist Authorization"
|
|
41
|
+
echo "--------------------------------"
|
|
42
|
+
|
|
43
|
+
# Test 1: Whitelisted agent (namespaced)
|
|
44
|
+
test_case \
|
|
45
|
+
"Namespaced agent (hivr:bytebee) authorized" \
|
|
46
|
+
"200\|success" \
|
|
47
|
+
"curl -s '$API_URL/api/discover?query=web&agentId=hivr:bytebee'"
|
|
48
|
+
|
|
49
|
+
# Test 2: Whitelisted agent (legacy format)
|
|
50
|
+
test_case \
|
|
51
|
+
"Legacy agent (bytebee) authorized" \
|
|
52
|
+
"200\|success" \
|
|
53
|
+
"curl -s '$API_URL/api/discover?query=web&agentId=bytebee'"
|
|
54
|
+
|
|
55
|
+
# Test 3: Unauthorized agent
|
|
56
|
+
test_case \
|
|
57
|
+
"Unauthorized agent denied" \
|
|
58
|
+
"403\|Unauthorized\|Access Denied" \
|
|
59
|
+
"curl -s '$API_URL/api/discover?query=web&agentId=hacker:evil'"
|
|
60
|
+
|
|
61
|
+
echo ""
|
|
62
|
+
echo "2. Test Access Control"
|
|
63
|
+
echo "----------------------"
|
|
64
|
+
|
|
65
|
+
# Test 4: Hivr agent accessing allowed provider
|
|
66
|
+
test_case \
|
|
67
|
+
"Hivr agent accessing brave_search (allowed)" \
|
|
68
|
+
"200\|success" \
|
|
69
|
+
"curl -s -X POST '$API_URL/api/call_api' -H 'Content-Type: application/json' -d '{\"agentId\":\"hivr:bytebee\",\"provider\":\"brave_search\",\"action\":\"search\",\"params\":{\"query\":\"test\"}}'"
|
|
70
|
+
|
|
71
|
+
# Test 5: Restricted access (if NordSym configured)
|
|
72
|
+
# This will fail if NordSym not configured yet, which is OK
|
|
73
|
+
echo -n "Testing: NordSym agent restricted access ... "
|
|
74
|
+
if curl -s -X POST "$API_URL/api/call_api" \
|
|
75
|
+
-H "Content-Type: application/json" \
|
|
76
|
+
-d '{"agentId":"nordsym:test","provider":"restricted_api","action":"call","params":{}}' \
|
|
77
|
+
| grep -q "403\|Access Denied\|not whitelisted"; then
|
|
78
|
+
echo -e "${GREEN}✓ PASS${NC} (correctly denied)"
|
|
79
|
+
else
|
|
80
|
+
echo -e "${YELLOW}⊘ SKIP${NC} (NordSym not configured)"
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
echo ""
|
|
84
|
+
echo "3. Test Analytics"
|
|
85
|
+
echo "-----------------"
|
|
86
|
+
|
|
87
|
+
# Test 6: Product field in logs
|
|
88
|
+
if [ -f ~/.apiclaw/logs/api-calls.jsonl ]; then
|
|
89
|
+
recent_logs=$(tail -5 ~/.apiclaw/logs/api-calls.jsonl)
|
|
90
|
+
|
|
91
|
+
echo -n "Testing: Product field in logs ... "
|
|
92
|
+
if echo "$recent_logs" | grep -q '"product":"hivr"'; then
|
|
93
|
+
echo -e "${GREEN}✓ PASS${NC}"
|
|
94
|
+
else
|
|
95
|
+
echo -e "${YELLOW}⊘ WARN${NC} (no recent Hivr calls logged)"
|
|
96
|
+
fi
|
|
97
|
+
else
|
|
98
|
+
echo -e "${YELLOW}⊘ SKIP${NC} (no log file yet)"
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
echo ""
|
|
102
|
+
echo "4. Test Cache Behavior"
|
|
103
|
+
echo "----------------------"
|
|
104
|
+
|
|
105
|
+
# Test 7: Multiple requests use cache
|
|
106
|
+
echo -n "Testing: Cache performance ... "
|
|
107
|
+
start=$(date +%s%N)
|
|
108
|
+
for i in {1..5}; do
|
|
109
|
+
curl -s "$API_URL/api/discover?query=web&agentId=hivr:bytebee" > /dev/null
|
|
110
|
+
done
|
|
111
|
+
end=$(date +%s%N)
|
|
112
|
+
elapsed=$(( (end - start) / 1000000 )) # Convert to ms
|
|
113
|
+
|
|
114
|
+
if [ $elapsed -lt 1000 ]; then
|
|
115
|
+
echo -e "${GREEN}✓ PASS${NC} (${elapsed}ms for 5 requests)"
|
|
116
|
+
else
|
|
117
|
+
echo -e "${YELLOW}⊘ WARN${NC} (${elapsed}ms - cache might not be working)"
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
echo ""
|
|
121
|
+
echo "===================================="
|
|
122
|
+
echo "Test suite complete!"
|
|
123
|
+
echo ""
|
|
124
|
+
echo "Next steps:"
|
|
125
|
+
echo "1. Check logs: tail -f ~/.apiclaw/logs/api-calls.jsonl"
|
|
126
|
+
echo "2. Monitor analytics for product field"
|
|
127
|
+
echo "3. Add NordSym product source when ready"
|
|
128
|
+
echo ""
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access Control System
|
|
3
|
+
* Controls which products/agents can access which providers
|
|
4
|
+
*
|
|
5
|
+
* Rules format:
|
|
6
|
+
* - Wildcard: "hivr:*" = all Hivr agents
|
|
7
|
+
* - Specific: "hivr:bytebee" = only ByteBee
|
|
8
|
+
* - Product-level: "nordsym:*" = all NordSym agents
|
|
9
|
+
*
|
|
10
|
+
* Provider wildcards:
|
|
11
|
+
* - "*" = all providers
|
|
12
|
+
* - "brave_*" = all Brave providers
|
|
13
|
+
* - Specific: ["brave_search", "groq"]
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
interface AccessRule {
|
|
17
|
+
agentPattern: string;
|
|
18
|
+
allowedProviders: string[];
|
|
19
|
+
description?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Default access rules
|
|
23
|
+
// These can be moved to Convex table for dynamic updates
|
|
24
|
+
const DEFAULT_RULES: AccessRule[] = [
|
|
25
|
+
{
|
|
26
|
+
agentPattern: 'hivr:*',
|
|
27
|
+
allowedProviders: ['*'], // Hivr gets everything
|
|
28
|
+
description: 'All Hivr bees get full access',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
agentPattern: 'nordsym:*',
|
|
32
|
+
allowedProviders: ['brave_search', 'groq', 'replicate'],
|
|
33
|
+
description: 'NordSym team gets selected providers',
|
|
34
|
+
},
|
|
35
|
+
// Add more rules as needed
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// Cache for compiled rules
|
|
39
|
+
let compiledRules: {
|
|
40
|
+
pattern: RegExp;
|
|
41
|
+
providers: string[];
|
|
42
|
+
}[] | null = null;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Compile agentPattern to RegExp
|
|
46
|
+
*/
|
|
47
|
+
function compilePattern(pattern: string): RegExp {
|
|
48
|
+
// Convert wildcard pattern to regex
|
|
49
|
+
// "hivr:*" → /^hivr:.+$/
|
|
50
|
+
// "hivr:byte*" → /^hivr:byte.+$/
|
|
51
|
+
const escaped = pattern
|
|
52
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex chars
|
|
53
|
+
.replace(/\*/g, '.+'); // Replace * with .+
|
|
54
|
+
|
|
55
|
+
return new RegExp(`^${escaped}$`, 'i');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Compile all rules (cache for performance)
|
|
60
|
+
*/
|
|
61
|
+
function compileRules(): void {
|
|
62
|
+
compiledRules = DEFAULT_RULES.map(rule => ({
|
|
63
|
+
pattern: compilePattern(rule.agentPattern),
|
|
64
|
+
providers: rule.allowedProviders,
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if provider matches pattern
|
|
70
|
+
*/
|
|
71
|
+
function matchesProvider(provider: string, pattern: string): boolean {
|
|
72
|
+
if (pattern === '*') return true;
|
|
73
|
+
if (pattern.endsWith('*')) {
|
|
74
|
+
const prefix = pattern.slice(0, -1);
|
|
75
|
+
return provider.startsWith(prefix);
|
|
76
|
+
}
|
|
77
|
+
return provider === pattern;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if agentId is allowed to access provider
|
|
82
|
+
*/
|
|
83
|
+
export function canAccessProvider(agentId: string, provider: string): boolean {
|
|
84
|
+
if (!compiledRules) {
|
|
85
|
+
compileRules();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const normalized = agentId.toLowerCase().trim();
|
|
89
|
+
const normalizedProvider = provider.toLowerCase().trim();
|
|
90
|
+
|
|
91
|
+
// Find matching rule
|
|
92
|
+
for (const rule of compiledRules!) {
|
|
93
|
+
if (rule.pattern.test(normalized)) {
|
|
94
|
+
// Check if provider is allowed
|
|
95
|
+
for (const providerPattern of rule.providers) {
|
|
96
|
+
if (matchesProvider(normalizedProvider, providerPattern)) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Rule matched but provider not in allowlist
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// No rule matched = deny by default
|
|
106
|
+
console.warn(`[Access Control] No rule for ${normalized}`);
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get allowed providers for agentId
|
|
112
|
+
*/
|
|
113
|
+
export function getAllowedProviders(agentId: string): string[] {
|
|
114
|
+
if (!compiledRules) {
|
|
115
|
+
compileRules();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const normalized = agentId.toLowerCase().trim();
|
|
119
|
+
|
|
120
|
+
// Find matching rule
|
|
121
|
+
for (const rule of compiledRules!) {
|
|
122
|
+
if (rule.pattern.test(normalized)) {
|
|
123
|
+
return rule.providers;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Add new access rule (runtime)
|
|
132
|
+
*/
|
|
133
|
+
export function addAccessRule(rule: AccessRule): void {
|
|
134
|
+
DEFAULT_RULES.push(rule);
|
|
135
|
+
compiledRules = null; // Force recompile
|
|
136
|
+
console.log(`[Access Control] Added rule for ${rule.agentPattern}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get all access rules (for debugging/admin)
|
|
141
|
+
*/
|
|
142
|
+
export function getAccessRules(): AccessRule[] {
|
|
143
|
+
return [...DEFAULT_RULES];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Check if agentId + provider combination is allowed
|
|
148
|
+
* Combines whitelist check + access control
|
|
149
|
+
*/
|
|
150
|
+
export async function isAllowed(
|
|
151
|
+
agentId: string | undefined,
|
|
152
|
+
provider: string
|
|
153
|
+
): Promise<{ allowed: boolean; reason?: string }> {
|
|
154
|
+
if (!agentId) {
|
|
155
|
+
return { allowed: false, reason: 'No agentId provided' };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// First check: Is agent whitelisted?
|
|
159
|
+
const { isAuthorized } = await import('./product-whitelist.js');
|
|
160
|
+
const whitelisted = await isAuthorized(agentId);
|
|
161
|
+
|
|
162
|
+
if (!whitelisted) {
|
|
163
|
+
return { allowed: false, reason: 'Agent not whitelisted' };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Second check: Does agent have access to this provider?
|
|
167
|
+
const hasAccess = canAccessProvider(agentId, provider);
|
|
168
|
+
|
|
169
|
+
if (!hasAccess) {
|
|
170
|
+
return { allowed: false, reason: 'Provider not in access list' };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { allowed: true };
|
|
174
|
+
}
|
package/src/analytics.ts
CHANGED
|
@@ -18,6 +18,10 @@ export interface APICallLog {
|
|
|
18
18
|
success: boolean;
|
|
19
19
|
latencyMs?: number;
|
|
20
20
|
error?: string;
|
|
21
|
+
metadata?: {
|
|
22
|
+
product?: string;
|
|
23
|
+
[key: string]: any;
|
|
24
|
+
};
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
// Log directory
|
|
@@ -86,6 +90,7 @@ async function sendToConvex(log: APICallLog): Promise<void> {
|
|
|
86
90
|
latencyMs: log.latencyMs,
|
|
87
91
|
error: log.error,
|
|
88
92
|
timestamp: log.timestamp,
|
|
93
|
+
...log.metadata, // Include product and any other metadata
|
|
89
94
|
},
|
|
90
95
|
});
|
|
91
96
|
} catch (e) {
|
|
@@ -275,13 +275,23 @@ export async function mcpInstallCommand(options: MCPInstallOptions): Promise<voi
|
|
|
275
275
|
console.log(chalk.cyan('\n✅ Dry run complete! Run without --dry-run to apply changes.\n'));
|
|
276
276
|
} else if (successCount > 0) {
|
|
277
277
|
console.log(chalk.green('\n✅ APIClaw installed successfully!\n'));
|
|
278
|
-
console.log('
|
|
279
|
-
console.log('
|
|
280
|
-
console.log('
|
|
278
|
+
console.log(chalk.bold('What you get:\n'));
|
|
279
|
+
console.log(chalk.cyan(' 🔍 Search') + ' 22,000+ APIs indexed');
|
|
280
|
+
console.log(chalk.cyan(' 🌐 Open APIs') + ' 1,600 APIs - no keys needed');
|
|
281
|
+
console.log(chalk.cyan(' 🔑 Direct Call') + ' 18 premium APIs - we own the keys');
|
|
281
282
|
console.log('');
|
|
282
|
-
console.log('
|
|
283
|
+
console.log('Next:');
|
|
284
|
+
console.log(' 1. Restart your MCP client');
|
|
285
|
+
console.log(' 2. Try: "Find weather APIs"');
|
|
286
|
+
console.log('');
|
|
287
|
+
console.log('Docs: https://apiclaw.com/docs\n');
|
|
283
288
|
} else {
|
|
284
289
|
console.log(chalk.yellow('\n✅ APIClaw already installed in all clients.\n'));
|
|
290
|
+
console.log(chalk.bold('What you have:\n'));
|
|
291
|
+
console.log(chalk.cyan(' 🔍 Search') + ' 22,000+ APIs indexed');
|
|
292
|
+
console.log(chalk.cyan(' 🌐 Open APIs') + ' 1,600 APIs - no keys needed');
|
|
293
|
+
console.log(chalk.cyan(' 🔑 Direct Call') + ' 18 premium APIs - we own the keys');
|
|
294
|
+
console.log('');
|
|
285
295
|
console.log('Run with --force to reinstall (coming soon).\n');
|
|
286
296
|
}
|
|
287
297
|
} else {
|