@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.
Files changed (77) hide show
  1. package/AGENTS.md +50 -33
  2. package/README.md +22 -12
  3. package/SOUL.md +60 -19
  4. package/STATUS.md +91 -169
  5. package/convex/_generated/api.d.ts +6 -0
  6. package/convex/directCall.ts +598 -0
  7. package/convex/providers.ts +341 -26
  8. package/convex/schema.ts +87 -0
  9. package/convex/usage.ts +260 -0
  10. package/convex/waitlist.ts +55 -0
  11. package/data/combined-02-26.json +22102 -0
  12. package/data/night-expansion-02-26-06-batch2.json +1898 -0
  13. package/data/night-expansion-02-26-06-batch3.json +1410 -0
  14. package/data/night-expansion-02-26-06.json +3146 -0
  15. package/data/night-expansion-02-26-full.json +9726 -0
  16. package/data/night-expansion-02-26-v2.json +330 -0
  17. package/data/night-expansion-02-26.json +171 -0
  18. package/dist/crypto.d.ts +7 -0
  19. package/dist/crypto.d.ts.map +1 -0
  20. package/dist/crypto.js +67 -0
  21. package/dist/crypto.js.map +1 -0
  22. package/dist/execute-dynamic.d.ts +116 -0
  23. package/dist/execute-dynamic.d.ts.map +1 -0
  24. package/dist/execute-dynamic.js +456 -0
  25. package/dist/execute-dynamic.js.map +1 -0
  26. package/dist/execute.d.ts +2 -1
  27. package/dist/execute.d.ts.map +1 -1
  28. package/dist/execute.js +35 -5
  29. package/dist/execute.js.map +1 -1
  30. package/dist/index.js +33 -4
  31. package/dist/index.js.map +1 -1
  32. package/dist/registry/apis.json +2081 -3
  33. package/docs/PRD-customer-key-passthrough.md +184 -0
  34. package/landing/public/badges/available-on-apiclaw.svg +14 -0
  35. package/landing/scripts/generate-stats.js +75 -4
  36. package/landing/src/app/admin/page.tsx +1 -1
  37. package/landing/src/app/api/auth/magic-link/route.ts +1 -1
  38. package/landing/src/app/api/auth/session/route.ts +1 -1
  39. package/landing/src/app/api/auth/verify/route.ts +1 -1
  40. package/landing/src/app/api/og/route.tsx +5 -3
  41. package/landing/src/app/docs/page.tsx +5 -4
  42. package/landing/src/app/earn/page.tsx +14 -11
  43. package/landing/src/app/globals.css +16 -15
  44. package/landing/src/app/layout.tsx +2 -2
  45. package/landing/src/app/page.tsx +425 -254
  46. package/landing/src/app/providers/dashboard/[apiId]/actions/[actionId]/edit/page.tsx +600 -0
  47. package/landing/src/app/providers/dashboard/[apiId]/actions/new/page.tsx +583 -0
  48. package/landing/src/app/providers/dashboard/[apiId]/actions/page.tsx +301 -0
  49. package/landing/src/app/providers/dashboard/[apiId]/direct-call/page.tsx +659 -0
  50. package/landing/src/app/providers/dashboard/[apiId]/page.tsx +381 -0
  51. package/landing/src/app/providers/dashboard/[apiId]/test/page.tsx +418 -0
  52. package/landing/src/app/providers/dashboard/layout.tsx +292 -0
  53. package/landing/src/app/providers/dashboard/page.tsx +353 -290
  54. package/landing/src/app/providers/register/page.tsx +87 -10
  55. package/landing/src/components/AiClientDropdown.tsx +85 -0
  56. package/landing/src/components/ConfigHelperModal.tsx +113 -0
  57. package/landing/src/components/HeroTabs.tsx +187 -0
  58. package/landing/src/components/ShareIntegrationModal.tsx +198 -0
  59. package/landing/src/hooks/useDashboardData.ts +53 -1
  60. package/landing/src/lib/apis.json +46554 -174
  61. package/landing/src/lib/convex-client.ts +22 -3
  62. package/landing/src/lib/stats.json +4 -4
  63. package/landing/tsconfig.tsbuildinfo +1 -1
  64. package/night-expansion-02-26-06-batch2.py +368 -0
  65. package/night-expansion-02-26-06-batch3.py +299 -0
  66. package/night-expansion-02-26-06.py +756 -0
  67. package/package.json +1 -1
  68. package/scripts/bulk-add-public-apis-v2.py +418 -0
  69. package/scripts/night-expansion-02-26-v2.py +296 -0
  70. package/scripts/night-expansion-02-26.py +890 -0
  71. package/scripts/seed-complete-api.js +181 -0
  72. package/scripts/seed-demo-api.sh +44 -0
  73. package/src/crypto.ts +75 -0
  74. package/src/execute-dynamic.ts +589 -0
  75. package/src/execute.ts +41 -5
  76. package/src/index.ts +38 -4
  77. 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 all connected providers with their actions
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 - fallback to proxy if not available locally
779
- const creds = getCredentials(providerId);
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)) {