@nordsym/apiclaw 1.5.9 → 1.5.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG-WHITELIST-V2.md +269 -0
  2. package/HIVR-INTEGRATION.md +281 -0
  3. package/HIVR-WHITELIST-STATUS.md +205 -0
  4. package/HIVR-WHITELIST.md +148 -0
  5. package/WHITELIST-ARCHITECTURE.md +379 -0
  6. package/api/discover.ts +71 -0
  7. package/api/health.ts +20 -0
  8. package/convex/http.d.ts.map +1 -1
  9. package/convex/http.js +8 -0
  10. package/convex/http.js.map +1 -1
  11. package/convex/http.ts +8 -0
  12. package/dist/access-control.d.ts +45 -0
  13. package/dist/access-control.d.ts.map +1 -0
  14. package/dist/access-control.js +142 -0
  15. package/dist/access-control.js.map +1 -0
  16. package/dist/analytics.d.ts +4 -0
  17. package/dist/analytics.d.ts.map +1 -1
  18. package/dist/analytics.js +1 -0
  19. package/dist/analytics.js.map +1 -1
  20. package/dist/credentials.d.ts.map +1 -1
  21. package/dist/credentials.js +20 -0
  22. package/dist/credentials.js.map +1 -1
  23. package/dist/execute.d.ts.map +1 -1
  24. package/dist/execute.js +245 -0
  25. package/dist/execute.js.map +1 -1
  26. package/dist/hivr-whitelist.d.ts +18 -0
  27. package/dist/hivr-whitelist.d.ts.map +1 -0
  28. package/dist/hivr-whitelist.js +95 -0
  29. package/dist/hivr-whitelist.js.map +1 -0
  30. package/dist/http-api.d.ts.map +1 -1
  31. package/dist/http-api.js +17 -33
  32. package/dist/http-api.js.map +1 -1
  33. package/dist/http-server-minimal.d.ts +7 -0
  34. package/dist/http-server-minimal.d.ts.map +1 -0
  35. package/dist/http-server-minimal.js +126 -0
  36. package/dist/http-server-minimal.js.map +1 -0
  37. package/dist/product-whitelist.d.ts +37 -0
  38. package/dist/product-whitelist.d.ts.map +1 -0
  39. package/dist/product-whitelist.js +203 -0
  40. package/dist/product-whitelist.js.map +1 -0
  41. package/dist/proxy.d.ts.map +1 -1
  42. package/dist/proxy.js +1 -1
  43. package/dist/proxy.js.map +1 -1
  44. package/landing/next-env.d.ts +1 -0
  45. package/landing/pages/api/discover.ts +43 -0
  46. package/landing/pages/api/health.ts +20 -0
  47. package/landing/src/app/auth/verify/page.tsx +6 -0
  48. package/landing/src/app/dashboard/verify/page.tsx +6 -0
  49. package/landing/src/app/join/page.tsx +6 -0
  50. package/landing/src/app/mou/[partnerId]/page.tsx +6 -0
  51. package/landing/src/app/providers/dashboard/[apiId]/actions/[actionId]/edit/page.tsx +6 -0
  52. package/landing/src/app/providers/dashboard/[apiId]/actions/new/page.tsx +5 -0
  53. package/landing/src/app/providers/dashboard/[apiId]/actions/page.tsx +5 -0
  54. package/landing/src/app/providers/dashboard/[apiId]/direct-call/page.tsx +5 -0
  55. package/landing/src/app/providers/dashboard/[apiId]/page.tsx +5 -0
  56. package/landing/src/app/providers/dashboard/[apiId]/test/page.tsx +5 -0
  57. package/landing/src/app/providers/dashboard/layout.tsx +6 -6
  58. package/landing/src/app/providers/dashboard/verify/page.tsx +6 -0
  59. package/landing/src/app/upgrade/page.tsx +6 -0
  60. package/landing/src/app/workspace/page.tsx +6 -0
  61. package/landing/src/lib/stats.json +1 -1
  62. package/package.json +4 -2
  63. package/scripts/test-whitelist-v2.sh +128 -0
  64. package/src/access-control.ts +174 -0
  65. package/src/analytics.ts +5 -0
  66. package/src/credentials.ts +20 -0
  67. package/src/execute.ts +247 -0
  68. package/src/hivr-whitelist.ts +110 -0
  69. package/src/http-api.ts +18 -34
  70. package/src/http-server-minimal.ts +154 -0
  71. package/src/product-whitelist.ts +246 -0
  72. package/src/proxy.ts +1 -1
@@ -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.match(/\/providers\/dashboard\/([^/]+)/);
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.includes("/login") || pathname.includes("/verify")) {
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.startsWith(item.href.split('?')[0])) && !isApiDetailPage
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.includes("/direct-call")
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.includes("/actions")
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.includes("/test")
227
+ pathname?.includes("/test")
228
228
  ? "bg-accent text-white"
229
229
  : "text-text-secondary hover:bg-surface hover:text-text-primary"
230
230
  }`}
@@ -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
 
@@ -4,7 +4,7 @@
4
4
  "directCallCount": 18,
5
5
  "npmDownloads": 4232,
6
6
  "categoryCount": 13,
7
- "generatedAt": "2026-03-18T12:30:05.958Z",
7
+ "generatedAt": "2026-03-19T12:16:00.022Z",
8
8
  "categoryBreakdown": {
9
9
  "Finance": 1179,
10
10
  "Auth & Security": 491,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nordsym/apiclaw",
3
- "version": "1.5.9",
3
+ "version": "1.5.10",
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.32.0",
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) {
@@ -167,6 +167,22 @@ const providers: Record<string, ProviderCredential> = {
167
167
  return null;
168
168
  },
169
169
  },
170
+
171
+ apilayer: {
172
+ type: 'api_key',
173
+ get(): APICredentials | null {
174
+ const env = loadEnvFile('apilayer.env');
175
+ // Return all keys — handler picks the right one per action
176
+ const keys: Record<string, string> = {};
177
+ for (const [k, v] of Object.entries(env)) {
178
+ if (k.startsWith('APILAYER_') && v) {
179
+ keys[k] = v;
180
+ }
181
+ }
182
+ if (Object.keys(keys).length === 0) return null;
183
+ return { type: 'api_key', api_key: keys.APILAYER_EXCHANGERATE_KEY || '', ...keys } as any;
184
+ },
185
+ },
170
186
  };
171
187
 
172
188
  /**
@@ -215,6 +231,10 @@ export function hasRealCredentials(providerId: string): boolean {
215
231
  const env = loadEnvFile('e2b.env');
216
232
  return !!(env.E2B_API_KEY || process.env.E2B_API_KEY);
217
233
  }
234
+ if (providerId === 'apilayer') {
235
+ const env = loadEnvFile('apilayer.env');
236
+ return !!(env.APILAYER_EXCHANGERATE_KEY || process.env.APILAYER_EXCHANGERATE_KEY);
237
+ }
218
238
  return false;
219
239
  }
220
240