@nordsym/apiclaw 1.5.8 → 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.
- 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/HTTP-API.md +306 -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/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/bin-http.d.ts +7 -0
- package/dist/bin-http.d.ts.map +1 -0
- package/dist/bin-http.js +43 -0
- package/dist/bin-http.js.map +1 -0
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +20 -0
- package/dist/credentials.js.map +1 -1
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +245 -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 +16 -0
- package/dist/http-api.d.ts.map +1 -0
- package/dist/http-api.js +253 -0
- package/dist/http-api.js.map +1 -0
- 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/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/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 +5 -5
- package/landing/src/app/mou/[partnerId]/page.tsx +6 -0
- 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 +5 -0
- 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/verify/page.tsx +6 -0
- package/landing/src/app/upgrade/page.tsx +6 -0
- package/landing/src/app/workspace/page.tsx +6 -0
- package/landing/src/lib/stats.json +1 -1
- package/package.json +6 -3
- package/scripts/test-whitelist-v2.sh +128 -0
- package/src/access-control.ts +174 -0
- package/src/analytics.ts +5 -0
- package/src/bin-http.ts +45 -0
- package/src/credentials.ts +20 -0
- package/src/execute.ts +247 -0
- package/src/hivr-whitelist.ts +110 -0
- package/src/http-api.ts +286 -0
- package/src/http-server-minimal.ts +154 -0
- package/src/product-whitelist.ts +246 -0
- package/src/proxy.ts +1 -1
|
@@ -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) {
|
package/src/bin-http.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* APIClaw HTTP API Server - Standalone executable
|
|
4
|
+
* Usage: apiclaw-http [--port 3000]
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { startHTTPServer } from './http-api.js';
|
|
8
|
+
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
let port = 3000;
|
|
11
|
+
|
|
12
|
+
// Parse args
|
|
13
|
+
for (let i = 0; i < args.length; i++) {
|
|
14
|
+
if (args[i] === '--port' || args[i] === '-p') {
|
|
15
|
+
port = parseInt(args[i + 1] || '3000');
|
|
16
|
+
i++;
|
|
17
|
+
} else if (args[i] === '--help' || args[i] === '-h') {
|
|
18
|
+
console.log(`
|
|
19
|
+
🦞 APIClaw HTTP API Server
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
apiclaw-http [options]
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
--port, -p <port> Port to listen on (default: 3000)
|
|
26
|
+
--help, -h Show this help
|
|
27
|
+
|
|
28
|
+
Examples:
|
|
29
|
+
apiclaw-http
|
|
30
|
+
apiclaw-http --port 8080
|
|
31
|
+
|
|
32
|
+
Endpoints:
|
|
33
|
+
GET /api/discover?query=...&agentId=...
|
|
34
|
+
POST /api/call_api
|
|
35
|
+
Body: { provider, action, params, agentId }
|
|
36
|
+
GET /health
|
|
37
|
+
|
|
38
|
+
Auth:
|
|
39
|
+
Whitelist-based for Hivr bees. Contact admin@nordsym.com for access.
|
|
40
|
+
`);
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
startHTTPServer(port);
|
package/src/credentials.ts
CHANGED
|
@@ -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
|
|
package/src/execute.ts
CHANGED
|
@@ -402,6 +402,21 @@ const apiEndpoints: Record<string, Record<string, { url: string; method: string;
|
|
|
402
402
|
run_code: { url: 'https://api.e2b.dev/v1/sandboxes', method: 'POST' },
|
|
403
403
|
run_shell: { url: 'https://api.e2b.dev/v1/sandboxes', method: 'POST' },
|
|
404
404
|
},
|
|
405
|
+
apilayer: {
|
|
406
|
+
exchange_rates: { url: 'https://api.apilayer.com/exchangerates_data/latest', method: 'GET' },
|
|
407
|
+
market_data: { url: 'http://api.marketstack.com/v1/eod', method: 'GET' },
|
|
408
|
+
aviation: { url: 'http://api.aviationstack.com/v1/flights', method: 'GET' },
|
|
409
|
+
pdf_generate: { url: 'https://api.pdflayer.com/api/convert', method: 'GET' },
|
|
410
|
+
screenshot: { url: 'https://api.screenshotlayer.com/api/capture', method: 'GET' },
|
|
411
|
+
verify_email: { url: 'https://api.apilayer.com/email_verification/check', method: 'GET' },
|
|
412
|
+
verify_number: { url: 'https://api.apilayer.com/number_verification/validate', method: 'GET' },
|
|
413
|
+
vat_check: { url: 'http://apilayer.net/api/validate', method: 'GET' },
|
|
414
|
+
world_news: { url: 'https://api.apilayer.com/world_news/search-news', method: 'GET' },
|
|
415
|
+
finance_news: { url: 'https://api.apilayer.com/financelayer/news', method: 'GET' },
|
|
416
|
+
scrape: { url: 'https://api.apilayer.com/adv_scraper/scraper', method: 'GET' },
|
|
417
|
+
image_crop: { url: 'https://api.apilayer.com/image_crop/crop', method: 'GET' },
|
|
418
|
+
skills: { url: 'https://api.apilayer.com/skills', method: 'GET' },
|
|
419
|
+
},
|
|
405
420
|
};
|
|
406
421
|
|
|
407
422
|
/**
|
|
@@ -1644,6 +1659,238 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
1644
1659
|
};
|
|
1645
1660
|
},
|
|
1646
1661
|
},
|
|
1662
|
+
|
|
1663
|
+
// APILayer - 14 APIs via one provider
|
|
1664
|
+
apilayer: {
|
|
1665
|
+
// Helper to pick the right key per action
|
|
1666
|
+
exchange_rates: async (params, creds) => {
|
|
1667
|
+
const key = creds.APILAYER_EXCHANGERATE_KEY || creds.api_key;
|
|
1668
|
+
const { base = 'USD', symbols, date } = params;
|
|
1669
|
+
const endpoint = date ? 'historical' : 'latest';
|
|
1670
|
+
const url = new URL(`https://api.apilayer.com/exchangerates_data/${endpoint}`);
|
|
1671
|
+
url.searchParams.set('base', base);
|
|
1672
|
+
if (symbols) url.searchParams.set('symbols', symbols);
|
|
1673
|
+
if (date) url.searchParams.set('date', date);
|
|
1674
|
+
|
|
1675
|
+
const response = await fetchWithRetry(url.toString(), {
|
|
1676
|
+
headers: { 'apikey': key },
|
|
1677
|
+
}, { provider: 'apilayer', action: 'exchange_rates' });
|
|
1678
|
+
|
|
1679
|
+
const data = await response.json() as Record<string, unknown>;
|
|
1680
|
+
if (!response.ok) return createErrorResult('apilayer', 'exchange_rates', (data.message as string) || 'Request failed', statusToErrorCode(response.status));
|
|
1681
|
+
return { success: true, provider: 'apilayer', action: 'exchange_rates', data };
|
|
1682
|
+
},
|
|
1683
|
+
|
|
1684
|
+
market_data: async (params, creds) => {
|
|
1685
|
+
const key = creds.APILAYER_MARKETSTACK_KEY || creds.api_key;
|
|
1686
|
+
const { symbols, date_from, date_to, limit = 10 } = params;
|
|
1687
|
+
if (!symbols) return createErrorResult('apilayer', 'market_data', 'Missing required param: symbols', ERROR_CODES.INVALID_PARAMS);
|
|
1688
|
+
|
|
1689
|
+
const url = new URL('http://api.marketstack.com/v1/eod');
|
|
1690
|
+
url.searchParams.set('access_key', key);
|
|
1691
|
+
url.searchParams.set('symbols', symbols);
|
|
1692
|
+
url.searchParams.set('limit', limit.toString());
|
|
1693
|
+
if (date_from) url.searchParams.set('date_from', date_from);
|
|
1694
|
+
if (date_to) url.searchParams.set('date_to', date_to);
|
|
1695
|
+
|
|
1696
|
+
const response = await fetchWithRetry(url.toString(), {}, { provider: 'apilayer', action: 'market_data' });
|
|
1697
|
+
const data = await response.json() as Record<string, unknown>;
|
|
1698
|
+
if (!response.ok) return createErrorResult('apilayer', 'market_data', (data.error as any)?.message || 'Request failed', statusToErrorCode(response.status));
|
|
1699
|
+
return { success: true, provider: 'apilayer', action: 'market_data', data };
|
|
1700
|
+
},
|
|
1701
|
+
|
|
1702
|
+
aviation: async (params, creds) => {
|
|
1703
|
+
const key = creds.APILAYER_AVIATIONSTACK_KEY || creds.api_key;
|
|
1704
|
+
const { flight_iata, dep_iata, arr_iata, airline_iata } = params;
|
|
1705
|
+
const url = new URL('http://api.aviationstack.com/v1/flights');
|
|
1706
|
+
url.searchParams.set('access_key', key);
|
|
1707
|
+
if (flight_iata) url.searchParams.set('flight_iata', flight_iata);
|
|
1708
|
+
if (dep_iata) url.searchParams.set('dep_iata', dep_iata);
|
|
1709
|
+
if (arr_iata) url.searchParams.set('arr_iata', arr_iata);
|
|
1710
|
+
if (airline_iata) url.searchParams.set('airline_iata', airline_iata);
|
|
1711
|
+
|
|
1712
|
+
const response = await fetchWithRetry(url.toString(), {}, { provider: 'apilayer', action: 'aviation' });
|
|
1713
|
+
const data = await response.json() as Record<string, unknown>;
|
|
1714
|
+
if (!response.ok) return createErrorResult('apilayer', 'aviation', 'Request failed', statusToErrorCode(response.status));
|
|
1715
|
+
return { success: true, provider: 'apilayer', action: 'aviation', data };
|
|
1716
|
+
},
|
|
1717
|
+
|
|
1718
|
+
pdf_generate: async (params, creds) => {
|
|
1719
|
+
const key = creds.APILAYER_PDFLAYER_KEY || creds.api_key;
|
|
1720
|
+
const { document_url, document_html, page_size = 'A4' } = params;
|
|
1721
|
+
if (!document_url && !document_html) return createErrorResult('apilayer', 'pdf_generate', 'Missing: document_url or document_html', ERROR_CODES.INVALID_PARAMS);
|
|
1722
|
+
|
|
1723
|
+
const url = new URL('https://api.pdflayer.com/api/convert');
|
|
1724
|
+
url.searchParams.set('access_key', key);
|
|
1725
|
+
url.searchParams.set('page_size', page_size);
|
|
1726
|
+
if (document_url) url.searchParams.set('document_url', document_url);
|
|
1727
|
+
if (document_html) url.searchParams.set('document_html', document_html);
|
|
1728
|
+
|
|
1729
|
+
const response = await fetchWithRetry(url.toString(), {}, { provider: 'apilayer', action: 'pdf_generate' });
|
|
1730
|
+
const contentType = response.headers.get('content-type') || '';
|
|
1731
|
+
if (contentType.includes('application/pdf')) {
|
|
1732
|
+
return { success: true, provider: 'apilayer', action: 'pdf_generate', data: { message: 'PDF generated', content_type: 'application/pdf', size: response.headers.get('content-length') } };
|
|
1733
|
+
}
|
|
1734
|
+
const data = await response.json() as Record<string, unknown>;
|
|
1735
|
+
if (!response.ok) return createErrorResult('apilayer', 'pdf_generate', (data.error as any)?.info || 'Request failed', statusToErrorCode(response.status));
|
|
1736
|
+
return { success: true, provider: 'apilayer', action: 'pdf_generate', data };
|
|
1737
|
+
},
|
|
1738
|
+
|
|
1739
|
+
screenshot: async (params, creds) => {
|
|
1740
|
+
const key = creds.APILAYER_SCREENSHOTLAYER_KEY || creds.api_key;
|
|
1741
|
+
const { url: targetUrl, viewport = '1440x900', fullpage = 0 } = params;
|
|
1742
|
+
if (!targetUrl) return createErrorResult('apilayer', 'screenshot', 'Missing required param: url', ERROR_CODES.INVALID_PARAMS);
|
|
1743
|
+
|
|
1744
|
+
const url = new URL('https://api.screenshotlayer.com/api/capture');
|
|
1745
|
+
url.searchParams.set('access_key', key);
|
|
1746
|
+
url.searchParams.set('url', targetUrl);
|
|
1747
|
+
url.searchParams.set('viewport', viewport);
|
|
1748
|
+
url.searchParams.set('fullpage', fullpage.toString());
|
|
1749
|
+
|
|
1750
|
+
const response = await fetchWithRetry(url.toString(), {}, { provider: 'apilayer', action: 'screenshot' });
|
|
1751
|
+
const contentType = response.headers.get('content-type') || '';
|
|
1752
|
+
if (contentType.includes('image/')) {
|
|
1753
|
+
return { success: true, provider: 'apilayer', action: 'screenshot', data: { message: 'Screenshot captured', content_type: contentType, url: url.toString() } };
|
|
1754
|
+
}
|
|
1755
|
+
const data = await response.json() as Record<string, unknown>;
|
|
1756
|
+
return { success: true, provider: 'apilayer', action: 'screenshot', data };
|
|
1757
|
+
},
|
|
1758
|
+
|
|
1759
|
+
verify_email: async (params, creds) => {
|
|
1760
|
+
const key = creds.APILAYER_EMAILVERIFY_KEY || creds.api_key;
|
|
1761
|
+
const { email } = params;
|
|
1762
|
+
if (!email) return createErrorResult('apilayer', 'verify_email', 'Missing required param: email', ERROR_CODES.INVALID_PARAMS);
|
|
1763
|
+
|
|
1764
|
+
const url = new URL('https://api.apilayer.com/email_verification/check');
|
|
1765
|
+
url.searchParams.set('email', email);
|
|
1766
|
+
|
|
1767
|
+
const response = await fetchWithRetry(url.toString(), {
|
|
1768
|
+
headers: { 'apikey': key },
|
|
1769
|
+
}, { provider: 'apilayer', action: 'verify_email' });
|
|
1770
|
+
const data = await response.json() as Record<string, unknown>;
|
|
1771
|
+
if (!response.ok) return createErrorResult('apilayer', 'verify_email', 'Request failed', statusToErrorCode(response.status));
|
|
1772
|
+
return { success: true, provider: 'apilayer', action: 'verify_email', data };
|
|
1773
|
+
},
|
|
1774
|
+
|
|
1775
|
+
verify_number: async (params, creds) => {
|
|
1776
|
+
const key = creds.APILAYER_NUMVERIFY_KEY || creds.api_key;
|
|
1777
|
+
const { number } = params;
|
|
1778
|
+
if (!number) return createErrorResult('apilayer', 'verify_number', 'Missing required param: number', ERROR_CODES.INVALID_PARAMS);
|
|
1779
|
+
|
|
1780
|
+
const url = new URL('https://api.apilayer.com/number_verification/validate');
|
|
1781
|
+
url.searchParams.set('number', number);
|
|
1782
|
+
|
|
1783
|
+
const response = await fetchWithRetry(url.toString(), {
|
|
1784
|
+
headers: { 'apikey': key },
|
|
1785
|
+
}, { provider: 'apilayer', action: 'verify_number' });
|
|
1786
|
+
const data = await response.json() as Record<string, unknown>;
|
|
1787
|
+
if (!response.ok) return createErrorResult('apilayer', 'verify_number', 'Request failed', statusToErrorCode(response.status));
|
|
1788
|
+
return { success: true, provider: 'apilayer', action: 'verify_number', data };
|
|
1789
|
+
},
|
|
1790
|
+
|
|
1791
|
+
vat_check: async (params, creds) => {
|
|
1792
|
+
const key = creds.APILAYER_VATLAYER_KEY || creds.api_key;
|
|
1793
|
+
const { vat_number } = params;
|
|
1794
|
+
if (!vat_number) return createErrorResult('apilayer', 'vat_check', 'Missing required param: vat_number', ERROR_CODES.INVALID_PARAMS);
|
|
1795
|
+
|
|
1796
|
+
const url = new URL('http://apilayer.net/api/validate');
|
|
1797
|
+
url.searchParams.set('access_key', key);
|
|
1798
|
+
url.searchParams.set('vat_number', vat_number);
|
|
1799
|
+
|
|
1800
|
+
const response = await fetchWithRetry(url.toString(), {}, { provider: 'apilayer', action: 'vat_check' });
|
|
1801
|
+
const data = await response.json() as Record<string, unknown>;
|
|
1802
|
+
if (!response.ok) return createErrorResult('apilayer', 'vat_check', 'Request failed', statusToErrorCode(response.status));
|
|
1803
|
+
return { success: true, provider: 'apilayer', action: 'vat_check', data };
|
|
1804
|
+
},
|
|
1805
|
+
|
|
1806
|
+
world_news: async (params, creds) => {
|
|
1807
|
+
const key = creds.APILAYER_WORLDNEWS_KEY || creds.api_key;
|
|
1808
|
+
const { text, source_countries, language = 'en', number = 5 } = params;
|
|
1809
|
+
|
|
1810
|
+
const url = new URL('https://api.apilayer.com/world_news/search-news');
|
|
1811
|
+
if (text) url.searchParams.set('text', text);
|
|
1812
|
+
if (source_countries) url.searchParams.set('source-countries', source_countries);
|
|
1813
|
+
url.searchParams.set('language', language);
|
|
1814
|
+
url.searchParams.set('number', number.toString());
|
|
1815
|
+
|
|
1816
|
+
const response = await fetchWithRetry(url.toString(), {
|
|
1817
|
+
headers: { 'apikey': key },
|
|
1818
|
+
}, { provider: 'apilayer', action: 'world_news' });
|
|
1819
|
+
const data = await response.json() as Record<string, unknown>;
|
|
1820
|
+
if (!response.ok) return createErrorResult('apilayer', 'world_news', 'Request failed', statusToErrorCode(response.status));
|
|
1821
|
+
return { success: true, provider: 'apilayer', action: 'world_news', data };
|
|
1822
|
+
},
|
|
1823
|
+
|
|
1824
|
+
finance_news: async (params, creds) => {
|
|
1825
|
+
const key = creds.APILAYER_FINANCENEWS_KEY || creds.api_key;
|
|
1826
|
+
const { tickers, text, number = 5 } = params;
|
|
1827
|
+
|
|
1828
|
+
const url = new URL('https://api.apilayer.com/financelayer/news');
|
|
1829
|
+
if (tickers) url.searchParams.set('tickers', tickers);
|
|
1830
|
+
if (text) url.searchParams.set('keywords', text);
|
|
1831
|
+
url.searchParams.set('limit', number.toString());
|
|
1832
|
+
|
|
1833
|
+
const response = await fetchWithRetry(url.toString(), {
|
|
1834
|
+
headers: { 'apikey': key },
|
|
1835
|
+
}, { provider: 'apilayer', action: 'finance_news' });
|
|
1836
|
+
const data = await response.json() as Record<string, unknown>;
|
|
1837
|
+
if (!response.ok) return createErrorResult('apilayer', 'finance_news', 'Request failed', statusToErrorCode(response.status));
|
|
1838
|
+
return { success: true, provider: 'apilayer', action: 'finance_news', data };
|
|
1839
|
+
},
|
|
1840
|
+
|
|
1841
|
+
scrape: async (params, creds) => {
|
|
1842
|
+
const key = creds.APILAYER_SCRAPER_KEY || creds.api_key;
|
|
1843
|
+
const { url: targetUrl } = params;
|
|
1844
|
+
if (!targetUrl) return createErrorResult('apilayer', 'scrape', 'Missing required param: url', ERROR_CODES.INVALID_PARAMS);
|
|
1845
|
+
|
|
1846
|
+
const url = new URL('https://api.apilayer.com/adv_scraper/scraper');
|
|
1847
|
+
url.searchParams.set('url', targetUrl);
|
|
1848
|
+
|
|
1849
|
+
const response = await fetchWithRetry(url.toString(), {
|
|
1850
|
+
headers: { 'apikey': key },
|
|
1851
|
+
}, { provider: 'apilayer', action: 'scrape' });
|
|
1852
|
+
const data = await response.json() as Record<string, unknown>;
|
|
1853
|
+
if (!response.ok) return createErrorResult('apilayer', 'scrape', 'Request failed', statusToErrorCode(response.status));
|
|
1854
|
+
return { success: true, provider: 'apilayer', action: 'scrape', data };
|
|
1855
|
+
},
|
|
1856
|
+
|
|
1857
|
+
image_crop: async (params, creds) => {
|
|
1858
|
+
const key = creds.APILAYER_IMAGECROP_KEY || creds.api_key;
|
|
1859
|
+
const { url: imageUrl, width, height } = params;
|
|
1860
|
+
if (!imageUrl) return createErrorResult('apilayer', 'image_crop', 'Missing required param: url', ERROR_CODES.INVALID_PARAMS);
|
|
1861
|
+
|
|
1862
|
+
const url = new URL('https://api.apilayer.com/image_crop/crop');
|
|
1863
|
+
url.searchParams.set('url', imageUrl);
|
|
1864
|
+
if (width) url.searchParams.set('width', width.toString());
|
|
1865
|
+
if (height) url.searchParams.set('height', height.toString());
|
|
1866
|
+
|
|
1867
|
+
const response = await fetchWithRetry(url.toString(), {
|
|
1868
|
+
headers: { 'apikey': key },
|
|
1869
|
+
}, { provider: 'apilayer', action: 'image_crop' });
|
|
1870
|
+
const contentType = response.headers.get('content-type') || '';
|
|
1871
|
+
if (contentType.includes('image/')) {
|
|
1872
|
+
return { success: true, provider: 'apilayer', action: 'image_crop', data: { message: 'Image cropped', content_type: contentType } };
|
|
1873
|
+
}
|
|
1874
|
+
const data = await response.json() as Record<string, unknown>;
|
|
1875
|
+
return { success: true, provider: 'apilayer', action: 'image_crop', data };
|
|
1876
|
+
},
|
|
1877
|
+
|
|
1878
|
+
skills: async (params, creds) => {
|
|
1879
|
+
const key = creds.APILAYER_SKILLAPI_KEY || creds.api_key;
|
|
1880
|
+
const { q } = params;
|
|
1881
|
+
if (!q) return createErrorResult('apilayer', 'skills', 'Missing required param: q', ERROR_CODES.INVALID_PARAMS);
|
|
1882
|
+
|
|
1883
|
+
const url = new URL('https://api.apilayer.com/skills');
|
|
1884
|
+
url.searchParams.set('q', q);
|
|
1885
|
+
|
|
1886
|
+
const response = await fetchWithRetry(url.toString(), {
|
|
1887
|
+
headers: { 'apikey': key },
|
|
1888
|
+
}, { provider: 'apilayer', action: 'skills' });
|
|
1889
|
+
const data = await response.json() as Record<string, unknown>;
|
|
1890
|
+
if (!response.ok) return createErrorResult('apilayer', 'skills', 'Request failed', statusToErrorCode(response.status));
|
|
1891
|
+
return { success: true, provider: 'apilayer', action: 'skills', data };
|
|
1892
|
+
},
|
|
1893
|
+
},
|
|
1647
1894
|
};
|
|
1648
1895
|
|
|
1649
1896
|
// Get available actions for a provider (static handlers only)
|