@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
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)
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Hivr Bees Auto-Whitelist
3
+ * Dynamically fetches active agents from Hivr's Convex deployment
4
+ * Falls back to static whitelist if Convex is unreachable
5
+ */
6
+
7
+ // Hivr PROD Convex deployment
8
+ const HIVR_CONVEX_URL = "https://sensible-quail-275.convex.cloud";
9
+
10
+ // Fallback static whitelist (in case Convex is down)
11
+ const STATIC_WHITELIST = [
12
+ 'bytebee',
13
+ 'analyzerbee',
14
+ 'buildbee',
15
+ 'buzzwriter',
16
+ 'hivemind',
17
+ 'hivesage',
18
+ 'symbot',
19
+ 'hivrqueen',
20
+ 'marketmaven',
21
+ 'reconbee',
22
+ 'sprintbee',
23
+ 'quillbee',
24
+ ];
25
+
26
+ // Cache whitelist for 5 minutes
27
+ let cachedWhitelist: string[] | null = null;
28
+ let cacheExpiry: number = 0;
29
+
30
+ /**
31
+ * Fetch all active agents from Hivr Convex
32
+ */
33
+ async function fetchHivrAgents(): Promise<string[]> {
34
+ try {
35
+ // Call Convex HTTP API directly
36
+ const response = await fetch(`${HIVR_CONVEX_URL}/api/query`, {
37
+ method: 'POST',
38
+ headers: {
39
+ 'Content-Type': 'application/json',
40
+ },
41
+ body: JSON.stringify({
42
+ path: 'agents:list',
43
+ args: {},
44
+ }),
45
+ });
46
+
47
+ if (!response.ok) {
48
+ console.warn('[Hivr Whitelist] Convex HTTP API error, using static whitelist');
49
+ return STATIC_WHITELIST;
50
+ }
51
+
52
+ const agents = await response.json() as any[];
53
+
54
+ if (!agents || !Array.isArray(agents)) {
55
+ console.warn('[Hivr Whitelist] Invalid response from Hivr Convex, using static whitelist');
56
+ return STATIC_WHITELIST;
57
+ }
58
+
59
+ // Extract handles (Hivr uses 'handle', not 'agentId')
60
+ const handles = agents
61
+ .map((a: any) => a.handle?.toLowerCase().trim())
62
+ .filter((h: string | undefined) => h && h.length > 0);
63
+
64
+ console.log(`[Hivr Whitelist] Fetched ${handles.length} agents from Hivr`);
65
+ return handles;
66
+
67
+ } catch (error) {
68
+ console.error('[Hivr Whitelist] Failed to fetch from Hivr Convex:', error);
69
+ return STATIC_WHITELIST;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Get current whitelist (cached or fresh)
75
+ */
76
+ export async function getWhitelist(): Promise<string[]> {
77
+ const now = Date.now();
78
+
79
+ // Return cached if still valid
80
+ if (cachedWhitelist && now < cacheExpiry) {
81
+ return cachedWhitelist;
82
+ }
83
+
84
+ // Fetch fresh whitelist
85
+ cachedWhitelist = await fetchHivrAgents();
86
+ cacheExpiry = now + (5 * 60 * 1000); // 5 minutes
87
+
88
+ return cachedWhitelist;
89
+ }
90
+
91
+ /**
92
+ * Check if agent is authorized
93
+ */
94
+ export async function isAuthorized(agentId: string | undefined): Promise<boolean> {
95
+ if (!agentId) return false;
96
+
97
+ const whitelist = await getWhitelist();
98
+ const normalized = agentId.toLowerCase().trim();
99
+
100
+ return whitelist.includes(normalized);
101
+ }
102
+
103
+ /**
104
+ * Force refresh whitelist (call after adding new bee)
105
+ */
106
+ export function invalidateCache(): void {
107
+ cachedWhitelist = null;
108
+ cacheExpiry = 0;
109
+ console.log('[Hivr Whitelist] Cache invalidated');
110
+ }
package/src/http-api.ts CHANGED
@@ -17,23 +17,7 @@ import { isOpenAPI, executeOpenAPI } from './open-apis.js';
17
17
  import { executeMetered } from './metered.js';
18
18
  import { logAPICall } from './analytics.js';
19
19
  import { getMachineFingerprint } from './session.js';
20
-
21
- // Hivr bees whitelist - these agents get free unlimited access
22
- const HIVR_BEES_WHITELIST = [
23
- 'bytebee',
24
- 'analyzerbee',
25
- 'buildbee',
26
- 'buzzwriter',
27
- 'hivemind',
28
- 'hivesage',
29
- 'symbot',
30
- 'hivrqueen',
31
- 'marketmaven',
32
- 'reconbee',
33
- 'sprintbee',
34
- 'quillbee',
35
- // Add more as Hivr grows
36
- ];
20
+ import { isAuthorized, getProduct } from './product-whitelist.js';
37
21
 
38
22
  interface APIRequest {
39
23
  provider: string;
@@ -42,15 +26,6 @@ interface APIRequest {
42
26
  agentId: string;
43
27
  }
44
28
 
45
- /**
46
- * Check if agent is authorized (Hivr bee)
47
- */
48
- function isAuthorized(agentId: string | undefined): boolean {
49
- if (!agentId) return false;
50
- const normalized = agentId.toLowerCase().trim();
51
- return HIVR_BEES_WHITELIST.includes(normalized);
52
- }
53
-
54
29
  /**
55
30
  * Parse JSON body from request
56
31
  */
@@ -97,7 +72,7 @@ async function handleDiscover(req: IncomingMessage, res: ServerResponse, url: UR
97
72
  return;
98
73
  }
99
74
 
100
- if (!isAuthorized(agentId || undefined)) {
75
+ if (!(await isAuthorized(agentId || undefined))) {
101
76
  sendJSON(res, 403, {
102
77
  error: 'Unauthorized',
103
78
  message: 'This endpoint is restricted to Hivr bees. Contact admin@nordsym.com for access.',
@@ -109,15 +84,17 @@ async function handleDiscover(req: IncomingMessage, res: ServerResponse, url: UR
109
84
  const results = discoverAPIs(query, { category, maxResults });
110
85
  const responseTimeMs = Date.now() - startTime;
111
86
 
112
- // Log to analytics
87
+ // Log to analytics with product info
88
+ const product = agentId ? getProduct(agentId) : null;
113
89
  logAPICall({
114
90
  timestamp: new Date().toISOString(),
115
91
  provider: 'apiclaw_discovery',
116
92
  action: 'discover',
117
93
  type: 'open',
118
- userId: `hivr:${agentId}`,
94
+ userId: agentId || 'unknown',
119
95
  success: true,
120
96
  latencyMs: responseTimeMs,
97
+ metadata: product ? { product } : undefined,
121
98
  });
122
99
 
123
100
  sendJSON(res, 200, {
@@ -158,10 +135,15 @@ async function handleCallAPI(req: IncomingMessage, res: ServerResponse): Promise
158
135
  return;
159
136
  }
160
137
 
161
- if (!isAuthorized(agentId)) {
138
+ // Check whitelist + access control
139
+ const { isAllowed } = await import('./access-control.js');
140
+ const accessCheck = await isAllowed(agentId, provider);
141
+
142
+ if (!accessCheck.allowed) {
162
143
  sendJSON(res, 403, {
163
- error: 'Unauthorized',
164
- message: 'This endpoint is restricted to Hivr bees. Contact admin@nordsym.com for access.',
144
+ error: 'Access Denied',
145
+ message: accessCheck.reason || 'Not authorized',
146
+ hint: 'Contact admin@nordsym.com for access',
165
147
  });
166
148
  return;
167
149
  }
@@ -201,16 +183,18 @@ async function handleCallAPI(req: IncomingMessage, res: ServerResponse): Promise
201
183
 
202
184
  const latencyMs = Date.now() - startTime;
203
185
 
204
- // Log to analytics
186
+ // Log to analytics with product info
187
+ const product = getProduct(agentId);
205
188
  logAPICall({
206
189
  timestamp: new Date().toISOString(),
207
190
  provider,
208
191
  action,
209
192
  type: apiType!,
210
- userId: `hivr:${agentId}`,
193
+ userId: agentId,
211
194
  success,
212
195
  latencyMs,
213
196
  error,
197
+ metadata: product ? { product } : undefined,
214
198
  });
215
199
 
216
200
  sendJSON(res, success ? 200 : 500, {
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Minimal HTTP API Server for APIClaw
4
+ * Bypasses chain executor imports
5
+ */
6
+
7
+ import { createServer } from 'http';
8
+ import { URL } from 'url';
9
+
10
+ const PORT = parseInt(process.env.PORT || '3001');
11
+
12
+ // Import whitelist directly
13
+ import { isAuthorized, getProduct } from './product-whitelist.js';
14
+
15
+ interface APIRequest {
16
+ provider: string;
17
+ action: string;
18
+ params: Record<string, any>;
19
+ agentId: string;
20
+ }
21
+
22
+ function sendJSON(res: any, status: number, data: any): void {
23
+ res.writeHead(status, {
24
+ 'Content-Type': 'application/json',
25
+ 'Access-Control-Allow-Origin': '*',
26
+ });
27
+ res.end(JSON.stringify(data));
28
+ }
29
+
30
+ async function parseBody<T>(req: any): Promise<T> {
31
+ return new Promise((resolve, reject) => {
32
+ let body = '';
33
+ req.on('data', (chunk: any) => body += chunk.toString());
34
+ req.on('end', () => {
35
+ try {
36
+ resolve(JSON.parse(body));
37
+ } catch (e) {
38
+ reject(new Error('Invalid JSON'));
39
+ }
40
+ });
41
+ });
42
+ }
43
+
44
+ const server = createServer(async (req, res) => {
45
+ const url = new URL(req.url || '/', `http://${req.headers.host}`);
46
+
47
+ console.log(`[APIClaw] ${req.method} ${url.pathname}`);
48
+
49
+ // CORS
50
+ if (req.method === 'OPTIONS') {
51
+ res.writeHead(204, {
52
+ 'Access-Control-Allow-Origin': '*',
53
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
54
+ 'Access-Control-Allow-Headers': 'Content-Type',
55
+ });
56
+ res.end();
57
+ return;
58
+ }
59
+
60
+ // Health check
61
+ if (url.pathname === '/health') {
62
+ sendJSON(res, 200, {
63
+ status: 'ok',
64
+ service: 'apiclaw-http-api',
65
+ version: '2.0.0',
66
+ whitelist: 'multi-product',
67
+ });
68
+ return;
69
+ }
70
+
71
+ // Discovery endpoint
72
+ if (url.pathname === '/api/discover' && req.method === 'GET') {
73
+ const query = url.searchParams.get('query');
74
+ const agentId = url.searchParams.get('agentId');
75
+
76
+ if (!query) {
77
+ sendJSON(res, 400, { error: 'Missing query parameter' });
78
+ return;
79
+ }
80
+
81
+ const authorized = await isAuthorized(agentId || undefined);
82
+
83
+ if (!authorized) {
84
+ sendJSON(res, 403, {
85
+ error: 'Unauthorized',
86
+ message: 'This endpoint is restricted. Contact admin@nordsym.com',
87
+ });
88
+ return;
89
+ }
90
+
91
+ const product = agentId ? getProduct(agentId) : null;
92
+
93
+ sendJSON(res, 200, {
94
+ success: true,
95
+ query,
96
+ agentId,
97
+ product,
98
+ message: 'Whitelist v2.0 active - discovery endpoint placeholder',
99
+ });
100
+ return;
101
+ }
102
+
103
+ // Call API endpoint
104
+ if (url.pathname === '/api/call_api' && req.method === 'POST') {
105
+ try {
106
+ const body = await parseBody<APIRequest>(req);
107
+ const { provider, action, params, agentId } = body;
108
+
109
+ if (!provider || !action || !agentId) {
110
+ sendJSON(res, 400, {
111
+ error: 'Missing required fields',
112
+ required: ['provider', 'action', 'agentId', 'params'],
113
+ });
114
+ return;
115
+ }
116
+
117
+ const authorized = await isAuthorized(agentId);
118
+
119
+ if (!authorized) {
120
+ sendJSON(res, 403, {
121
+ error: 'Unauthorized',
122
+ message: 'Agent not whitelisted',
123
+ });
124
+ return;
125
+ }
126
+
127
+ const product = getProduct(agentId);
128
+
129
+ sendJSON(res, 200, {
130
+ success: true,
131
+ agentId,
132
+ provider,
133
+ action,
134
+ product,
135
+ message: 'Whitelist v2.0 active - execution placeholder',
136
+ });
137
+
138
+ } catch (e: any) {
139
+ sendJSON(res, 400, { error: e.message });
140
+ }
141
+ return;
142
+ }
143
+
144
+ // 404
145
+ sendJSON(res, 404, { error: 'Not found' });
146
+ });
147
+
148
+ server.listen(PORT, () => {
149
+ console.log(`\nšŸ¦ž APIClaw HTTP API (Whitelist v2.0)`);
150
+ console.log(` Running on http://localhost:${PORT}`);
151
+ console.log(` GET /health`);
152
+ console.log(` GET /api/discover?query=...&agentId=...`);
153
+ console.log(` POST /api/call_api\n`);
154
+ });