@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,589 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* APIClaw Dynamic Executor
|
|
3
|
+
* Executes provider-configured actions via self-service Direct Call
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { decryptKey, validateBaseUrl } from './crypto.js';
|
|
7
|
+
|
|
8
|
+
// Types for dynamic provider config
|
|
9
|
+
export interface ProviderDirectCallConfig {
|
|
10
|
+
_id: string;
|
|
11
|
+
providerId: string;
|
|
12
|
+
apiId: string;
|
|
13
|
+
baseUrl: string;
|
|
14
|
+
authType: 'bearer' | 'basic' | 'api_key' | 'none';
|
|
15
|
+
authHeader: string;
|
|
16
|
+
authPrefix: string;
|
|
17
|
+
encryptedMasterKey: string;
|
|
18
|
+
rateLimitPerUser: number;
|
|
19
|
+
rateLimitPerDay: number;
|
|
20
|
+
pricePerRequest: number;
|
|
21
|
+
status: 'draft' | 'testing' | 'live';
|
|
22
|
+
// Customer key passthrough settings
|
|
23
|
+
allowCustomerKeys?: boolean; // Allow agents to pass their own API key (default: true)
|
|
24
|
+
requireCustomerKeys?: boolean; // Require customer key, no master key fallback (default: false)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ActionParam {
|
|
28
|
+
name: string;
|
|
29
|
+
type: 'string' | 'number' | 'boolean' | 'object';
|
|
30
|
+
required: boolean;
|
|
31
|
+
description: string;
|
|
32
|
+
default?: unknown;
|
|
33
|
+
in: 'body' | 'query' | 'path';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ResponseMapping {
|
|
37
|
+
name: string;
|
|
38
|
+
path: string; // JSONPath expression
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ProviderAction {
|
|
42
|
+
_id: string;
|
|
43
|
+
directCallId: string;
|
|
44
|
+
name: string;
|
|
45
|
+
displayName: string;
|
|
46
|
+
description: string;
|
|
47
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
48
|
+
path: string;
|
|
49
|
+
params: ActionParam[];
|
|
50
|
+
responseMapping: ResponseMapping[];
|
|
51
|
+
enabled: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ExecuteResult {
|
|
55
|
+
success: boolean;
|
|
56
|
+
provider: string;
|
|
57
|
+
action: string;
|
|
58
|
+
data?: unknown;
|
|
59
|
+
error?: string;
|
|
60
|
+
cost?: number;
|
|
61
|
+
latencyMs?: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface UsageStats {
|
|
65
|
+
minute: number;
|
|
66
|
+
day: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Convex HTTP API
|
|
70
|
+
const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || 'https://brilliant-puffin-712.eu-west-1.convex.cloud';
|
|
71
|
+
|
|
72
|
+
async function convexQuery<T>(path: string, args: Record<string, unknown>): Promise<T | null> {
|
|
73
|
+
try {
|
|
74
|
+
const res = await fetch(`${CONVEX_URL}/api/query`, {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: { 'Content-Type': 'application/json' },
|
|
77
|
+
body: JSON.stringify({ path, args }),
|
|
78
|
+
});
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
console.error(`Convex query failed: ${res.status}`);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const data = await res.json() as { value?: T } | T;
|
|
84
|
+
return (data && typeof data === 'object' && 'value' in data) ? data.value as T : data as T;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('Convex query error:', error);
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function convexMutation(path: string, args: Record<string, unknown>): Promise<boolean> {
|
|
92
|
+
try {
|
|
93
|
+
const res = await fetch(`${CONVEX_URL}/api/mutation`, {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers: { 'Content-Type': 'application/json' },
|
|
96
|
+
body: JSON.stringify({ path, args }),
|
|
97
|
+
});
|
|
98
|
+
return res.ok;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error('Convex mutation error:', error);
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if a provider has dynamic (self-service) config
|
|
107
|
+
*/
|
|
108
|
+
export async function hasDynamicConfig(providerId: string): Promise<boolean> {
|
|
109
|
+
const config = await getProviderConfig(providerId);
|
|
110
|
+
return config !== null && config.status === 'live';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Fetch provider direct call configuration from Convex
|
|
115
|
+
*/
|
|
116
|
+
export async function getProviderConfig(providerId: string): Promise<ProviderDirectCallConfig | null> {
|
|
117
|
+
// First try by API slug (for agent execution by name)
|
|
118
|
+
const bySlug = await convexQuery<ProviderDirectCallConfig>('directCall:getByApiSlug', { slug: providerId });
|
|
119
|
+
// Check for error response from Convex (not a real config)
|
|
120
|
+
const bySlugAny = bySlug as any;
|
|
121
|
+
if (bySlug && !(bySlugAny.status === 'error' || bySlugAny.errorMessage)) {
|
|
122
|
+
return bySlug;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Only try direct provider ID lookup if it looks like a Convex ID (starts with valid prefix)
|
|
126
|
+
// Convex IDs typically look like: k97xxx...
|
|
127
|
+
if (providerId.match(/^[a-z][a-z0-9]{2,}/)) {
|
|
128
|
+
const byId = await convexQuery<ProviderDirectCallConfig>('directCall:getDirectCallConfig', { providerId });
|
|
129
|
+
const byIdAny = byId as any;
|
|
130
|
+
if (byId && !(byIdAny.status === 'error' || byIdAny.errorMessage)) {
|
|
131
|
+
return byId;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Fetch action configuration from Convex
|
|
140
|
+
*/
|
|
141
|
+
export async function getActionConfig(providerId: string, actionName: string): Promise<ProviderAction | null> {
|
|
142
|
+
// First get the direct call config to get directCallId
|
|
143
|
+
const config = await getProviderConfig(providerId);
|
|
144
|
+
if (!config) return null;
|
|
145
|
+
|
|
146
|
+
return convexQuery<ProviderAction>('directCall:getActionByName', {
|
|
147
|
+
directCallId: config._id,
|
|
148
|
+
name: actionName
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get user's current usage for rate limiting
|
|
154
|
+
*/
|
|
155
|
+
export async function getUserUsage(userId: string, providerId: string): Promise<UsageStats> {
|
|
156
|
+
const usage = await convexQuery<UsageStats>('usage:getUserUsage', { userId, providerId });
|
|
157
|
+
return usage || { minute: 0, day: 0 };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Build the full URL with path and query parameters
|
|
162
|
+
*/
|
|
163
|
+
export function buildUrl(
|
|
164
|
+
baseUrl: string,
|
|
165
|
+
path: string,
|
|
166
|
+
params: Record<string, unknown>,
|
|
167
|
+
paramDefs: ActionParam[]
|
|
168
|
+
): string {
|
|
169
|
+
let finalPath = path;
|
|
170
|
+
const queryParams = new URLSearchParams();
|
|
171
|
+
|
|
172
|
+
for (const paramDef of paramDefs) {
|
|
173
|
+
const value = params[paramDef.name] ?? paramDef.default;
|
|
174
|
+
if (value === undefined) continue;
|
|
175
|
+
|
|
176
|
+
const stringValue = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
177
|
+
|
|
178
|
+
if (paramDef.in === 'path') {
|
|
179
|
+
// Replace path parameter: /users/{id} -> /users/123
|
|
180
|
+
finalPath = finalPath.replace(`{${paramDef.name}}`, encodeURIComponent(stringValue));
|
|
181
|
+
} else if (paramDef.in === 'query') {
|
|
182
|
+
queryParams.set(paramDef.name, stringValue);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Ensure baseUrl doesn't end with slash and path starts with slash
|
|
187
|
+
const cleanBase = baseUrl.replace(/\/$/, '');
|
|
188
|
+
const cleanPath = finalPath.startsWith('/') ? finalPath : `/${finalPath}`;
|
|
189
|
+
|
|
190
|
+
const queryString = queryParams.toString();
|
|
191
|
+
return queryString ? `${cleanBase}${cleanPath}?${queryString}` : `${cleanBase}${cleanPath}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Build request body from parameters
|
|
196
|
+
*/
|
|
197
|
+
export function buildBody(
|
|
198
|
+
params: Record<string, unknown>,
|
|
199
|
+
paramDefs: ActionParam[]
|
|
200
|
+
): string | undefined {
|
|
201
|
+
const bodyParams: Record<string, unknown> = {};
|
|
202
|
+
|
|
203
|
+
for (const paramDef of paramDefs) {
|
|
204
|
+
if (paramDef.in === 'body') {
|
|
205
|
+
const value = params[paramDef.name] ?? paramDef.default;
|
|
206
|
+
if (value !== undefined) {
|
|
207
|
+
bodyParams[paramDef.name] = value;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (Object.keys(bodyParams).length === 0) {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return JSON.stringify(bodyParams);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Build authentication headers based on auth type
|
|
221
|
+
*/
|
|
222
|
+
export function buildAuthHeaders(
|
|
223
|
+
config: ProviderDirectCallConfig,
|
|
224
|
+
decryptedKey: string
|
|
225
|
+
): Record<string, string> {
|
|
226
|
+
const headers: Record<string, string> = {
|
|
227
|
+
'Content-Type': 'application/json',
|
|
228
|
+
'User-Agent': 'APIClaw/1.0',
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const headerName = config.authHeader || 'Authorization';
|
|
232
|
+
const prefix = config.authPrefix || '';
|
|
233
|
+
|
|
234
|
+
switch (config.authType) {
|
|
235
|
+
case 'bearer':
|
|
236
|
+
headers[headerName] = prefix ? `${prefix} ${decryptedKey}` : `Bearer ${decryptedKey}`;
|
|
237
|
+
break;
|
|
238
|
+
|
|
239
|
+
case 'basic':
|
|
240
|
+
// Assume decryptedKey is "username:password"
|
|
241
|
+
const base64 = Buffer.from(decryptedKey).toString('base64');
|
|
242
|
+
headers[headerName] = `Basic ${base64}`;
|
|
243
|
+
break;
|
|
244
|
+
|
|
245
|
+
case 'api_key':
|
|
246
|
+
// Custom header with the key directly
|
|
247
|
+
headers[headerName] = prefix ? `${prefix} ${decryptedKey}` : decryptedKey;
|
|
248
|
+
break;
|
|
249
|
+
|
|
250
|
+
case 'none':
|
|
251
|
+
// No auth header needed
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return headers;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Extract value from object using simple JSONPath-like expression
|
|
260
|
+
* Supports: $.field, $.field.nested, $.array[0], $.array[*].field
|
|
261
|
+
*/
|
|
262
|
+
export function extractJsonPath(data: unknown, path: string): unknown {
|
|
263
|
+
if (!path.startsWith('$')) {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const parts = path.slice(1).split('.').filter(Boolean);
|
|
268
|
+
let current: unknown = data;
|
|
269
|
+
|
|
270
|
+
for (const part of parts) {
|
|
271
|
+
if (current === null || current === undefined) {
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Handle array index: field[0] or field[*]
|
|
276
|
+
const arrayMatch = part.match(/^(\w+)\[(\d+|\*)\]$/);
|
|
277
|
+
if (arrayMatch) {
|
|
278
|
+
const [, field, index] = arrayMatch;
|
|
279
|
+
if (typeof current !== 'object') return undefined;
|
|
280
|
+
current = (current as Record<string, unknown>)[field];
|
|
281
|
+
|
|
282
|
+
if (!Array.isArray(current)) return undefined;
|
|
283
|
+
|
|
284
|
+
if (index === '*') {
|
|
285
|
+
// Return all elements (will need further processing)
|
|
286
|
+
continue;
|
|
287
|
+
} else {
|
|
288
|
+
current = current[parseInt(index)];
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
if (typeof current !== 'object') return undefined;
|
|
292
|
+
current = (current as Record<string, unknown>)[part];
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return current;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Map response data using configured response mappings
|
|
301
|
+
*/
|
|
302
|
+
export function mapResponse(
|
|
303
|
+
data: unknown,
|
|
304
|
+
responseMapping: ResponseMapping[]
|
|
305
|
+
): Record<string, unknown> {
|
|
306
|
+
if (!responseMapping || responseMapping.length === 0) {
|
|
307
|
+
// No mapping configured, return raw data
|
|
308
|
+
return { raw: data };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const result: Record<string, unknown> = {};
|
|
312
|
+
|
|
313
|
+
for (const mapping of responseMapping) {
|
|
314
|
+
const value = extractJsonPath(data, mapping.path);
|
|
315
|
+
if (value !== undefined) {
|
|
316
|
+
result[mapping.name] = value;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Log usage to Convex
|
|
325
|
+
*/
|
|
326
|
+
export async function logUsage(params: {
|
|
327
|
+
userId: string;
|
|
328
|
+
providerId: string;
|
|
329
|
+
actionName: string;
|
|
330
|
+
timestamp: number;
|
|
331
|
+
success: boolean;
|
|
332
|
+
latencyMs: number;
|
|
333
|
+
creditsUsed: number;
|
|
334
|
+
}): Promise<void> {
|
|
335
|
+
await convexMutation('usage:logUsage', params);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Main function: Execute a dynamically configured action
|
|
340
|
+
*/
|
|
341
|
+
export async function executeDynamicAction(
|
|
342
|
+
providerId: string,
|
|
343
|
+
actionName: string,
|
|
344
|
+
params: Record<string, unknown>,
|
|
345
|
+
userId: string,
|
|
346
|
+
customerKey?: string
|
|
347
|
+
): Promise<ExecuteResult> {
|
|
348
|
+
const startTime = Date.now();
|
|
349
|
+
|
|
350
|
+
// 1. Get provider config
|
|
351
|
+
const config = await getProviderConfig(providerId);
|
|
352
|
+
if (!config) {
|
|
353
|
+
return {
|
|
354
|
+
success: false,
|
|
355
|
+
provider: providerId,
|
|
356
|
+
action: actionName,
|
|
357
|
+
error: 'Provider not found'
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (config.status !== 'live') {
|
|
362
|
+
return {
|
|
363
|
+
success: false,
|
|
364
|
+
provider: providerId,
|
|
365
|
+
action: actionName,
|
|
366
|
+
error: 'Provider not available (not live)'
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// 2. Validate base URL (SSRF prevention)
|
|
371
|
+
const urlValidation = validateBaseUrl(config.baseUrl);
|
|
372
|
+
if (!urlValidation.valid) {
|
|
373
|
+
return {
|
|
374
|
+
success: false,
|
|
375
|
+
provider: providerId,
|
|
376
|
+
action: actionName,
|
|
377
|
+
error: `Invalid provider URL: ${urlValidation.error}`
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 3. Check rate limits
|
|
382
|
+
const usage = await getUserUsage(userId, providerId);
|
|
383
|
+
if (usage.minute >= config.rateLimitPerUser) {
|
|
384
|
+
return {
|
|
385
|
+
success: false,
|
|
386
|
+
provider: providerId,
|
|
387
|
+
action: actionName,
|
|
388
|
+
error: 'Rate limit exceeded (per minute)'
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (usage.day >= config.rateLimitPerDay) {
|
|
392
|
+
return {
|
|
393
|
+
success: false,
|
|
394
|
+
provider: providerId,
|
|
395
|
+
action: actionName,
|
|
396
|
+
error: 'Rate limit exceeded (daily)'
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// 4. Get action config
|
|
401
|
+
const action = await getActionConfig(providerId, actionName);
|
|
402
|
+
if (!action) {
|
|
403
|
+
return {
|
|
404
|
+
success: false,
|
|
405
|
+
provider: providerId,
|
|
406
|
+
action: actionName,
|
|
407
|
+
error: 'Action not found'
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (!action.enabled) {
|
|
412
|
+
return {
|
|
413
|
+
success: false,
|
|
414
|
+
provider: providerId,
|
|
415
|
+
action: actionName,
|
|
416
|
+
error: 'Action is disabled'
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// 5. Validate required parameters
|
|
421
|
+
for (const paramDef of action.params) {
|
|
422
|
+
if (paramDef.required && params[paramDef.name] === undefined && paramDef.default === undefined) {
|
|
423
|
+
return {
|
|
424
|
+
success: false,
|
|
425
|
+
provider: providerId,
|
|
426
|
+
action: actionName,
|
|
427
|
+
error: `Missing required parameter: ${paramDef.name}`
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// 6. Resolve API key (customer key takes priority over master key)
|
|
433
|
+
let apiKey: string;
|
|
434
|
+
let usingCustomerKey = false;
|
|
435
|
+
|
|
436
|
+
// Check if provider requires customer keys (like CoAccept)
|
|
437
|
+
const requiresCustomerKey = config.requireCustomerKeys === true;
|
|
438
|
+
const allowsCustomerKey = config.allowCustomerKeys !== false; // Default true
|
|
439
|
+
|
|
440
|
+
if (customerKey && allowsCustomerKey) {
|
|
441
|
+
// Customer provided their own key - use it, skip usage tracking
|
|
442
|
+
apiKey = customerKey;
|
|
443
|
+
usingCustomerKey = true;
|
|
444
|
+
} else if (requiresCustomerKey) {
|
|
445
|
+
// Provider requires customer key but none provided
|
|
446
|
+
return {
|
|
447
|
+
success: false,
|
|
448
|
+
provider: providerId,
|
|
449
|
+
action: actionName,
|
|
450
|
+
error: 'This provider requires your own API key. Pass it via customer_key parameter.'
|
|
451
|
+
};
|
|
452
|
+
} else if (config.encryptedMasterKey) {
|
|
453
|
+
// Use provider's master key - track usage for billing
|
|
454
|
+
try {
|
|
455
|
+
apiKey = decryptKey(config.encryptedMasterKey);
|
|
456
|
+
} catch (error) {
|
|
457
|
+
console.error('Failed to decrypt provider key:', error);
|
|
458
|
+
return {
|
|
459
|
+
success: false,
|
|
460
|
+
provider: providerId,
|
|
461
|
+
action: actionName,
|
|
462
|
+
error: 'Provider configuration error'
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
} else {
|
|
466
|
+
// No key available
|
|
467
|
+
return {
|
|
468
|
+
success: false,
|
|
469
|
+
provider: providerId,
|
|
470
|
+
action: actionName,
|
|
471
|
+
error: 'No API key available. Provide your own key via customer_key parameter.'
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// 7. Build request
|
|
476
|
+
const url = buildUrl(config.baseUrl, action.path, params, action.params);
|
|
477
|
+
const headers = buildAuthHeaders(config, apiKey);
|
|
478
|
+
const body = action.method === 'GET' ? undefined : buildBody(params, action.params);
|
|
479
|
+
|
|
480
|
+
// 8. Execute request
|
|
481
|
+
let response: Response;
|
|
482
|
+
try {
|
|
483
|
+
response = await fetch(url, {
|
|
484
|
+
method: action.method,
|
|
485
|
+
headers,
|
|
486
|
+
body,
|
|
487
|
+
});
|
|
488
|
+
} catch (error) {
|
|
489
|
+
const latencyMs = Date.now() - startTime;
|
|
490
|
+
// Only log usage when using master key (for billing)
|
|
491
|
+
if (!usingCustomerKey) {
|
|
492
|
+
await logUsage({
|
|
493
|
+
userId,
|
|
494
|
+
providerId,
|
|
495
|
+
actionName,
|
|
496
|
+
timestamp: Date.now(),
|
|
497
|
+
success: false,
|
|
498
|
+
latencyMs,
|
|
499
|
+
creditsUsed: 0
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
success: false,
|
|
504
|
+
provider: providerId,
|
|
505
|
+
action: actionName,
|
|
506
|
+
error: `Network error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
507
|
+
latencyMs
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const latencyMs = Date.now() - startTime;
|
|
512
|
+
|
|
513
|
+
// 9. Parse response
|
|
514
|
+
let data: unknown;
|
|
515
|
+
try {
|
|
516
|
+
const contentType = response.headers.get('content-type');
|
|
517
|
+
if (contentType?.includes('application/json')) {
|
|
518
|
+
data = await response.json();
|
|
519
|
+
} else {
|
|
520
|
+
data = await response.text();
|
|
521
|
+
}
|
|
522
|
+
} catch {
|
|
523
|
+
data = null;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// 10. Log usage (only when using master key for billing)
|
|
527
|
+
if (!usingCustomerKey) {
|
|
528
|
+
await logUsage({
|
|
529
|
+
userId,
|
|
530
|
+
providerId,
|
|
531
|
+
actionName,
|
|
532
|
+
timestamp: Date.now(),
|
|
533
|
+
success: response.ok,
|
|
534
|
+
latencyMs,
|
|
535
|
+
creditsUsed: response.ok ? config.pricePerRequest : 0
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// 11. Handle error response
|
|
540
|
+
if (!response.ok) {
|
|
541
|
+
let errorMessage = `HTTP ${response.status}`;
|
|
542
|
+
if (data && typeof data === 'object') {
|
|
543
|
+
const errorObj = data as Record<string, unknown>;
|
|
544
|
+
errorMessage = (errorObj.message as string) ||
|
|
545
|
+
(errorObj.error as string) ||
|
|
546
|
+
(errorObj.detail as string) ||
|
|
547
|
+
errorMessage;
|
|
548
|
+
}
|
|
549
|
+
return {
|
|
550
|
+
success: false,
|
|
551
|
+
provider: providerId,
|
|
552
|
+
action: actionName,
|
|
553
|
+
error: errorMessage,
|
|
554
|
+
latencyMs
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// 12. Map response and return
|
|
559
|
+
const mappedData = mapResponse(data, action.responseMapping);
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
success: true,
|
|
563
|
+
provider: providerId,
|
|
564
|
+
action: actionName,
|
|
565
|
+
data: mappedData,
|
|
566
|
+
cost: config.pricePerRequest,
|
|
567
|
+
latencyMs
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* List available actions for a dynamic provider
|
|
573
|
+
*/
|
|
574
|
+
export async function listDynamicActions(providerId: string): Promise<string[]> {
|
|
575
|
+
const config = await getProviderConfig(providerId);
|
|
576
|
+
if (!config || config.status !== 'live') {
|
|
577
|
+
return [];
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const actions = await convexQuery<ProviderAction[]>('directCall:getActions', {
|
|
581
|
+
directCallId: config._id
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
if (!actions) return [];
|
|
585
|
+
|
|
586
|
+
return actions
|
|
587
|
+
.filter(a => a.enabled)
|
|
588
|
+
.map(a => a.name);
|
|
589
|
+
}
|
package/src/execute.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { getCredentials } from './credentials.js';
|
|
6
6
|
import { callProxy, PROXY_PROVIDERS } from './proxy.js';
|
|
7
|
+
import { executeDynamicAction, hasDynamicConfig, listDynamicActions } from './execute-dynamic.js';
|
|
7
8
|
|
|
8
9
|
interface ExecuteResult {
|
|
9
10
|
success: boolean;
|
|
@@ -734,12 +735,24 @@ const handlers: Record<string, Record<string, (params: any, creds: any) => Promi
|
|
|
734
735
|
},
|
|
735
736
|
};
|
|
736
737
|
|
|
737
|
-
// Get available actions for a provider
|
|
738
|
+
// Get available actions for a provider (static handlers only)
|
|
738
739
|
export function getProviderActions(providerId: string): string[] {
|
|
739
740
|
return Object.keys(handlers[providerId] || {});
|
|
740
741
|
}
|
|
741
742
|
|
|
742
|
-
// Get
|
|
743
|
+
// Get available actions for a provider (includes dynamic providers)
|
|
744
|
+
export async function getProviderActionsAsync(providerId: string): Promise<string[]> {
|
|
745
|
+
// First check static handlers
|
|
746
|
+
const staticActions = Object.keys(handlers[providerId] || {});
|
|
747
|
+
if (staticActions.length > 0) {
|
|
748
|
+
return staticActions;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Then check dynamic providers
|
|
752
|
+
return listDynamicActions(providerId);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Get all connected providers with their actions (static handlers only)
|
|
743
756
|
export function getConnectedProviders(): { provider: string; actions: string[] }[] {
|
|
744
757
|
return Object.entries(handlers).map(([provider, actions]) => ({
|
|
745
758
|
provider,
|
|
@@ -751,11 +764,32 @@ export function getConnectedProviders(): { provider: string; actions: string[] }
|
|
|
751
764
|
export async function executeAPICall(
|
|
752
765
|
providerId: string,
|
|
753
766
|
action: string,
|
|
754
|
-
params: Record<string, any
|
|
767
|
+
params: Record<string, any>,
|
|
768
|
+
userId?: string,
|
|
769
|
+
customerKey?: string
|
|
755
770
|
): Promise<ExecuteResult> {
|
|
771
|
+
// Check for dynamic (self-service) provider config first
|
|
772
|
+
if (userId) {
|
|
773
|
+
const isDynamic = await hasDynamicConfig(providerId);
|
|
774
|
+
if (isDynamic) {
|
|
775
|
+
return executeDynamicAction(providerId, action, params, userId, customerKey);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Fall back to hardcoded handlers
|
|
756
780
|
// Check if provider exists
|
|
757
781
|
const providerHandlers = handlers[providerId];
|
|
758
782
|
if (!providerHandlers) {
|
|
783
|
+
// Check if it might be a dynamic provider without userId
|
|
784
|
+
const dynamicActions = await listDynamicActions(providerId);
|
|
785
|
+
if (dynamicActions.length > 0) {
|
|
786
|
+
return {
|
|
787
|
+
success: false,
|
|
788
|
+
provider: providerId,
|
|
789
|
+
action,
|
|
790
|
+
error: `Provider '${providerId}' requires userId for dynamic execution. Available actions: ${dynamicActions.join(', ')}`,
|
|
791
|
+
};
|
|
792
|
+
}
|
|
759
793
|
return {
|
|
760
794
|
success: false,
|
|
761
795
|
provider: providerId,
|
|
@@ -775,8 +809,10 @@ export async function executeAPICall(
|
|
|
775
809
|
};
|
|
776
810
|
}
|
|
777
811
|
|
|
778
|
-
// Get credentials -
|
|
779
|
-
|
|
812
|
+
// Get credentials - customer key takes priority, then local secrets, then proxy
|
|
813
|
+
let creds = customerKey ? { apiKey: customerKey, apiSecret: '' } : getCredentials(providerId);
|
|
814
|
+
const usingCustomerKey = !!customerKey;
|
|
815
|
+
|
|
780
816
|
if (!creds) {
|
|
781
817
|
// Try proxy for supported providers
|
|
782
818
|
if (PROXY_PROVIDERS.includes(providerId)) {
|