@nordsym/apiclaw 2.0.0 → 2.1.0

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/src/index.ts CHANGED
@@ -84,6 +84,7 @@ interface WorkspaceContext {
84
84
 
85
85
  let workspaceContext: WorkspaceContext | null = null;
86
86
  let currentAgentId: string | null = null; // Agent ID from agents table (set on startup)
87
+ let pendingRegistrationEmail: string | null = null; // Email waiting for OTP verification
87
88
 
88
89
  // Anonymous rate limit tracking (in-memory, per machine fingerprint)
89
90
  interface AnonymousRateLimitState {
@@ -278,35 +279,21 @@ const rateLimitStore = new Map<string, RateLimitState>();
278
279
  const UNREGISTERED_CALL_LIMIT = 5;
279
280
 
280
281
  /**
281
- * For proxy providers, allow anonymous usage with rate limiting
282
+ * Check workspace access -- registration required for all API calls
282
283
  */
283
284
  function checkWorkspaceAccess(providerId?: string): { allowed: boolean; error?: string; isAnonymous?: boolean } {
284
- // Allow anonymous access for proxy providers
285
- if (providerId && PROXY_PROVIDERS.includes(providerId)) {
286
- if (!workspaceContext) {
287
- // Anonymous user - check rate limits
288
- const fingerprint = getMachineFingerprint();
289
- const rateLimitCheck = checkAnonymousRateLimit(fingerprint);
290
-
291
- if (!rateLimitCheck.allowed) {
292
- return {
293
- allowed: false,
294
- error: rateLimitCheck.error,
295
- isAnonymous: true
296
- };
297
- }
298
-
299
- return { allowed: true, isAnonymous: true };
300
- }
301
- // Authenticated user using proxy provider - allow with higher limits
302
- return { allowed: true, isAnonymous: false };
303
- }
304
-
305
- // Non-proxy providers require authentication
285
+ // All API calls require registration now
306
286
  if (!workspaceContext) {
307
- return {
308
- allowed: false,
309
- error: 'Not authenticated. Use register_owner to authenticate your workspace.'
287
+ return {
288
+ allowed: false,
289
+ error: JSON.stringify({
290
+ status: 'registration_required',
291
+ error: 'Registration required to call APIs.',
292
+ message: 'Ask the user for their email, then call register_owner({ email: "..." }). A 6-digit code will be sent. Then call verify_code with the code.',
293
+ action: 'register_owner',
294
+ free_tier: '50 API calls/month -- completely free.',
295
+ }, null, 2),
296
+ isAnonymous: true,
310
297
  };
311
298
  }
312
299
 
@@ -511,7 +498,7 @@ const tools: Tool[] = [
511
498
  },
512
499
  {
513
500
  name: 'call_api',
514
- description: `Execute an API call through APIClaw. Supports single calls AND multi-step chains.
501
+ description: `Execute an API call through APIClaw. Requires registration (free). If not registered, call register_owner first.
515
502
 
516
503
  SINGLE CALL: Provide provider + action + params
517
504
  CHAIN: Provide chain array to execute multiple APIs in sequence/parallel with cross-step references.
@@ -683,7 +670,7 @@ Example chain:
683
670
  // ============================================
684
671
  {
685
672
  name: 'register_owner',
686
- description: 'Register your email to create a workspace. This authenticates your agent with APIClaw. You will receive a magic link to verify ownership.',
673
+ description: 'REQUIRED before using any API. Register your email to create a workspace. A 6-digit verification code will be sent to your email. After calling this, ask the user for the code and call verify_code.',
687
674
  inputSchema: {
688
675
  type: 'object',
689
676
  properties: {
@@ -695,6 +682,24 @@ Example chain:
695
682
  required: ['email']
696
683
  }
697
684
  },
685
+ {
686
+ name: 'verify_code',
687
+ description: 'Verify the 6-digit code sent to your email after register_owner. This completes registration and activates your workspace. Ask the user to check their email and paste the code.',
688
+ inputSchema: {
689
+ type: 'object',
690
+ properties: {
691
+ email: {
692
+ type: 'string',
693
+ description: 'The email address used in register_owner'
694
+ },
695
+ code: {
696
+ type: 'string',
697
+ description: 'The 6-digit verification code from the email'
698
+ }
699
+ },
700
+ required: ['email', 'code']
701
+ }
702
+ },
698
703
  {
699
704
  name: 'check_workspace_status',
700
705
  description: 'Check your workspace status, tier, and usage remaining.',
@@ -831,40 +836,36 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
831
836
  try {
832
837
  switch (name) {
833
838
  case 'apiclaw_help': {
839
+ const isAuthenticated = !!workspaceContext;
834
840
  const helpText = `
835
- 🦞 APIClaw The API Layer for AI Agents
841
+ 🦞 APIClaw -- The API Layer for AI Agents
836
842
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
837
-
838
- DISCOVER APIs:
843
+ ${!isAuthenticated ? `
844
+ GET STARTED (free):
845
+ 1. register_owner({ email: "you@example.com" }) — sends 6-digit code
846
+ 2. verify_code({ email: "you@example.com", code: "123456" }) — activates workspace
847
+ ` : `
848
+ STATUS: Authenticated as ${workspaceContext!.email} (${workspaceContext!.tier} tier)
849
+ `}
850
+ DISCOVER APIs (free, no registration needed):
839
851
  discover_apis({ query: "send SMS to Sweden" })
840
- discover_apis({ query: "search the web", max_results: 10 })
841
852
  discover_apis({ query: "text to speech", category: "ai" })
842
853
 
843
- GET DETAILS:
844
- get_api_details({ api_id: "46elks" })
845
-
846
- DIRECT CALL (8 APIs, no key needed):
847
- get_connected_providers()
848
- call_api({ provider: "brave_search", endpoint: "search", params: { query: "AI agents" } })
849
-
850
- Available direct-call providers:
851
- brave_search Web search
852
- • 46elks — SMS (Sweden)
853
- twilio SMS (Global)
854
- • resend — Email
855
- • openrouter — LLM routing (100+ models)
856
- • elevenlabs — Text-to-speech
857
- • replicate — AI models (images, video, audio)
858
- • firecrawl — Web scraping & crawling
859
- • github — Code repos & developer data
860
- • e2b — Code sandbox for AI agents
861
-
862
- BROWSE:
863
- list_categories()
864
- list_all_apis({ category: "communication", limit: 20 })
854
+ CALL APIs (requires free registration):
855
+ call_api({ provider: "brave_search", action: "search", params: { q: "AI agents" } })
856
+ call_api({ provider: "elevenlabs", action: "tts", params: { text: "Hello" } })
857
+
858
+ 23 MANAGED PROVIDERS:
859
+ OpenAI, Anthropic, xAI/Grok, Groq, Mistral, OpenRouter, Together AI,
860
+ Replicate, ElevenLabs, Deepgram, AssemblyAI, Brave Search, Firecrawl,
861
+ Serper, Resend, 46elks, Twilio, E2B, Stability AI, Cohere, Voyage AI,
862
+ GitHub, APILayer (27 sub-APIs)
863
+
864
+ 26,700+ DISCOVERABLE | 1,654 CALLABLE | Free tier: 50 calls/month
865
865
 
866
866
  Docs: https://apiclaw.cloud
867
867
  `;
868
+
868
869
  return {
869
870
  content: [{ type: 'text', text: helpText }]
870
871
  };
@@ -1180,6 +1181,25 @@ Docs: https://apiclaw.cloud
1180
1181
  }
1181
1182
 
1182
1183
  case 'call_api': {
1184
+ // ============================================
1185
+ // REGISTRATION GATE: require email before any API call
1186
+ // ============================================
1187
+ if (!workspaceContext) {
1188
+ return {
1189
+ content: [{
1190
+ type: 'text',
1191
+ text: JSON.stringify({
1192
+ status: 'registration_required',
1193
+ error: 'You need to register before making API calls.',
1194
+ message: 'APIClaw requires a free account to use APIs. Ask the user for their email address, then call register_owner({ email: "user@example.com" }). A 6-digit verification code will be sent to their email. Then call verify_code with the code.',
1195
+ action: 'register_owner',
1196
+ free_tier: '50 API calls/month, unlimited discovery -- completely free.',
1197
+ }, null, 2)
1198
+ }],
1199
+ isError: true
1200
+ };
1201
+ }
1202
+
1183
1203
  const provider = args?.provider as string;
1184
1204
  const action = args?.action as string;
1185
1205
  const params = (args?.params as Record<string, any>) || {};
@@ -1188,7 +1208,7 @@ Docs: https://apiclaw.cloud
1188
1208
  const chain = args?.chain as ChainStepUnion[] | undefined;
1189
1209
  const subagentId = args?.subagent_id as string | undefined;
1190
1210
  const aiBackend = args?.ai_backend as string | undefined;
1191
-
1211
+
1192
1212
  // Track AI backend if provided
1193
1213
  if (aiBackend && workspaceContext?.sessionToken) {
1194
1214
  fetch('https://adventurous-avocet-799.convex.cloud/api/mutation', {
@@ -1663,13 +1683,29 @@ Docs: https://apiclaw.cloud
1663
1683
  }
1664
1684
 
1665
1685
  case 'capability': {
1686
+ // Registration gate
1687
+ if (!workspaceContext) {
1688
+ return {
1689
+ content: [{
1690
+ type: 'text',
1691
+ text: JSON.stringify({
1692
+ status: 'registration_required',
1693
+ error: 'You need to register before making API calls.',
1694
+ message: 'Ask the user for their email, then call register_owner({ email: "..." }). A 6-digit code will be sent. Then call verify_code with the code.',
1695
+ action: 'register_owner',
1696
+ }, null, 2)
1697
+ }],
1698
+ isError: true
1699
+ };
1700
+ }
1701
+
1666
1702
  const capabilityId = args?.capability as string;
1667
1703
  const action = args?.action as string;
1668
1704
  const params = (args?.params as Record<string, any>) || {};
1669
1705
  const preferences = (args?.preferences as Record<string, any>) || {};
1670
1706
  const subagentId = args?.subagent_id as string | undefined;
1671
1707
  const aiBackend = args?.ai_backend as string | undefined;
1672
-
1708
+
1673
1709
  // Track AI backend if provided
1674
1710
  if (aiBackend && workspaceContext?.sessionToken) {
1675
1711
  fetch('https://adventurous-avocet-799.convex.cloud/api/mutation', {
@@ -1755,7 +1791,7 @@ Docs: https://apiclaw.cloud
1755
1791
 
1756
1792
  case 'register_owner': {
1757
1793
  const email = args?.email as string;
1758
-
1794
+
1759
1795
  if (!email || !email.includes('@')) {
1760
1796
  return {
1761
1797
  content: [{
@@ -1768,13 +1804,12 @@ Docs: https://apiclaw.cloud
1768
1804
  isError: true
1769
1805
  };
1770
1806
  }
1771
-
1807
+
1772
1808
  try {
1773
- // Check if workspace already exists
1809
+ // Check if workspace already exists and is active -- auto-login
1774
1810
  const existing = await convex.query("workspaces:getByEmail" as any, { email }) as { id: string; status: string; tier: string; usageCount: number; usageLimit: number } | null;
1775
1811
 
1776
1812
  if (existing && existing.status === 'active') {
1777
- // Workspace exists and is active - create session directly
1778
1813
  const fingerprint = getMachineFingerprint();
1779
1814
  const sessionResult = await convex.mutation("workspaces:createAgentSession" as any, {
1780
1815
  workspaceId: existing.id,
@@ -1783,23 +1818,17 @@ Docs: https://apiclaw.cloud
1783
1818
 
1784
1819
  if (sessionResult.success) {
1785
1820
  writeSession(sessionResult.sessionToken!, existing.id, email);
1786
-
1787
- // Claim anonymous usage history
1821
+
1788
1822
  try {
1789
1823
  const claimResult = await convex.mutation("workspaces:claimAnonymousUsage" as any, {
1790
1824
  workspaceId: existing.id,
1791
1825
  machineFingerprint: fingerprint,
1792
- }) as { success: boolean; claimedCount?: number; message?: string };
1793
-
1826
+ }) as { success: boolean; claimedCount?: number };
1794
1827
  if (claimResult.success && claimResult.claimedCount) {
1795
- console.error(`[APIClaw] Claimed ${claimResult.claimedCount} anonymous usage records`);
1828
+ console.error(`[APIClaw] Claimed ${claimResult.claimedCount} anonymous usage records`);
1796
1829
  }
1797
- } catch (err) {
1798
- // Non-critical error, just log it
1799
- console.error('[APIClaw] Warning: Failed to claim anonymous usage:', err);
1800
- }
1801
-
1802
- // Update global context
1830
+ } catch (_) {}
1831
+
1803
1832
  workspaceContext = {
1804
1833
  sessionToken: sessionResult.sessionToken!,
1805
1834
  workspaceId: existing.id,
@@ -1809,7 +1838,7 @@ Docs: https://apiclaw.cloud
1809
1838
  usageCount: existing.usageCount,
1810
1839
  status: existing.status,
1811
1840
  };
1812
-
1841
+
1813
1842
  return {
1814
1843
  content: [{
1815
1844
  type: 'text',
@@ -1827,29 +1856,15 @@ Docs: https://apiclaw.cloud
1827
1856
  };
1828
1857
  }
1829
1858
  }
1830
-
1831
- // Create workspace and magic link
1832
- const createResult = await convex.mutation("workspaces:createWorkspace" as any, { email }) as { success: boolean; workspaceId?: string; error?: string };
1833
-
1834
- let workspaceId: string;
1835
- if (createResult.success) {
1836
- workspaceId = createResult.workspaceId!;
1837
- } else if (createResult.error === 'workspace_exists') {
1838
- workspaceId = createResult.workspaceId!;
1839
- } else {
1840
- throw new Error(createResult.error);
1841
- }
1842
-
1843
- // Create magic link
1859
+
1860
+ // New user or pending workspace -- send OTP
1844
1861
  const fingerprint = getMachineFingerprint();
1845
- const magicLinkResult = await convex.mutation("workspaces:createMagicLink" as any, {
1862
+ const otpResult = await convex.mutation("workspaces:createOTP" as any, {
1846
1863
  email,
1847
1864
  fingerprint,
1848
- }) as { token: string; expiresAt: number };
1849
-
1850
- // Send magic link via email
1851
- const verifyUrl = `https://apiclaw.cloud/auth/verify?token=${magicLinkResult.token}`;
1852
-
1865
+ }) as { code: string; expiresAt: number };
1866
+
1867
+ // Send OTP email
1853
1868
  const emailResponse = await fetch('https://api.resend.com/emails', {
1854
1869
  method: 'POST',
1855
1870
  headers: {
@@ -1859,25 +1874,42 @@ Docs: https://apiclaw.cloud
1859
1874
  body: JSON.stringify({
1860
1875
  from: 'APIClaw <noreply@apiclaw.cloud>',
1861
1876
  to: email,
1862
- subject: 'Verify your APIClaw workspace',
1863
- html: `<p>Click to verify: <a href="${verifyUrl}">${verifyUrl}</a></p><p>Expires in 15 minutes.</p>`
1877
+ subject: `Your APIClaw verification code: ${otpResult.code}`,
1878
+ html: `
1879
+ <div style="font-family: Inter, sans-serif; max-width: 480px; margin: 0 auto; padding: 40px 24px;">
1880
+ <div style="text-align: center; margin-bottom: 32px;">
1881
+ <span style="font-size: 48px;">🦞</span>
1882
+ </div>
1883
+ <h1 style="font-size: 24px; font-weight: 700; color: #0A0A0A; text-align: center; margin-bottom: 8px;">Your verification code</h1>
1884
+ <p style="font-size: 16px; color: #525252; text-align: center; margin-bottom: 32px;">Paste this code in your terminal to activate APIClaw.</p>
1885
+ <div style="background: #F5F5F5; border: 1px solid #E5E5E5; border-radius: 12px; padding: 24px; text-align: center; margin-bottom: 24px;">
1886
+ <code style="font-size: 36px; font-weight: 700; letter-spacing: 0.3em; color: #EF4444; font-family: 'JetBrains Mono', monospace;">${otpResult.code}</code>
1887
+ </div>
1888
+ <p style="font-size: 13px; color: #737373; text-align: center;">This code expires in 10 minutes. If you didn't request this, ignore this email.</p>
1889
+ <hr style="border: none; border-top: 1px solid #E5E5E5; margin: 32px 0 16px;" />
1890
+ <p style="font-size: 12px; color: #A3A3A3; text-align: center;">APIClaw -- The API Layer For AI Agents</p>
1891
+ </div>
1892
+ `
1864
1893
  })
1865
1894
  });
1866
-
1895
+
1867
1896
  if (!emailResponse.ok) {
1868
1897
  const errorData = await emailResponse.text();
1869
1898
  throw new Error(`Failed to send verification email: ${errorData}`);
1870
1899
  }
1871
-
1900
+
1901
+ // Store pending email for verify_code
1902
+ pendingRegistrationEmail = email;
1903
+
1872
1904
  return {
1873
1905
  content: [{
1874
1906
  type: 'text',
1875
1907
  text: JSON.stringify({
1876
- status: 'pending_verification',
1877
- message: 'Workspace created! Check your email for verification link.',
1908
+ status: 'code_sent',
1909
+ message: `Verification code sent to ${email}`,
1910
+ next_step: 'Ask the user to check their email for a 6-digit code, then call verify_code with the email and code.',
1878
1911
  email,
1879
- expires_in_minutes: 15,
1880
- next_step: 'Check your email, click the verification link, then run check_workspace_status',
1912
+ expires_in_minutes: 10,
1881
1913
  }, null, 2)
1882
1914
  }]
1883
1915
  };
@@ -1894,6 +1926,119 @@ Docs: https://apiclaw.cloud
1894
1926
  };
1895
1927
  }
1896
1928
  }
1929
+
1930
+ case 'verify_code': {
1931
+ const email = (args?.email as string) || pendingRegistrationEmail;
1932
+ const code = args?.code as string;
1933
+
1934
+ if (!email || !code) {
1935
+ return {
1936
+ content: [{
1937
+ type: 'text',
1938
+ text: JSON.stringify({
1939
+ status: 'error',
1940
+ error: 'Both email and code are required.',
1941
+ hint: 'Call register_owner first to receive a verification code.',
1942
+ }, null, 2)
1943
+ }],
1944
+ isError: true
1945
+ };
1946
+ }
1947
+
1948
+ try {
1949
+ const fingerprint = getMachineFingerprint();
1950
+ const result = await convex.mutation("workspaces:verifyOTP" as any, {
1951
+ email,
1952
+ code: code.trim(),
1953
+ fingerprint,
1954
+ }) as {
1955
+ success: boolean;
1956
+ error?: string;
1957
+ message?: string;
1958
+ isNewUser?: boolean;
1959
+ sessionToken?: string;
1960
+ workspace?: { id: string; email: string; tier: string; status: string; usageCount: number; usageLimit: number }
1961
+ };
1962
+
1963
+ if (!result.success) {
1964
+ // Increment attempt counter
1965
+ try {
1966
+ await convex.mutation("workspaces:incrementOTPAttempt" as any, { email, code: code.trim() });
1967
+ } catch (_) {}
1968
+
1969
+ return {
1970
+ content: [{
1971
+ type: 'text',
1972
+ text: JSON.stringify({
1973
+ status: 'error',
1974
+ error: result.message || 'Verification failed',
1975
+ hint: result.error === 'code_expired'
1976
+ ? 'Run register_owner again to get a new code.'
1977
+ : 'Check the code and try again.',
1978
+ }, null, 2)
1979
+ }],
1980
+ isError: true
1981
+ };
1982
+ }
1983
+
1984
+ // Success! Save session
1985
+ writeSession(result.sessionToken!, result.workspace!.id, result.workspace!.email);
1986
+
1987
+ // Claim anonymous usage
1988
+ try {
1989
+ const claimResult = await convex.mutation("workspaces:claimAnonymousUsage" as any, {
1990
+ workspaceId: result.workspace!.id,
1991
+ machineFingerprint: fingerprint,
1992
+ }) as { success: boolean; claimedCount?: number };
1993
+ if (claimResult.success && claimResult.claimedCount) {
1994
+ console.error(`[APIClaw] Claimed ${claimResult.claimedCount} anonymous usage records`);
1995
+ }
1996
+ } catch (_) {}
1997
+
1998
+ // Update global context
1999
+ workspaceContext = {
2000
+ sessionToken: result.sessionToken!,
2001
+ workspaceId: result.workspace!.id,
2002
+ email: result.workspace!.email,
2003
+ tier: result.workspace!.tier,
2004
+ usageRemaining: result.workspace!.usageLimit - result.workspace!.usageCount,
2005
+ usageCount: result.workspace!.usageCount,
2006
+ status: result.workspace!.status,
2007
+ };
2008
+
2009
+ pendingRegistrationEmail = null;
2010
+
2011
+ return {
2012
+ content: [{
2013
+ type: 'text',
2014
+ text: JSON.stringify({
2015
+ status: 'success',
2016
+ message: result.isNewUser
2017
+ ? `Welcome to APIClaw! Workspace activated for ${result.workspace!.email}`
2018
+ : `Welcome back! Authenticated as ${result.workspace!.email}`,
2019
+ workspace: {
2020
+ email: result.workspace!.email,
2021
+ tier: result.workspace!.tier,
2022
+ usageCount: result.workspace!.usageCount,
2023
+ usageLimit: result.workspace!.usageLimit,
2024
+ },
2025
+ ready: 'You can now use discover_apis and call_api.',
2026
+ }, null, 2)
2027
+ }]
2028
+ };
2029
+ } catch (error) {
2030
+ return {
2031
+ content: [{
2032
+ type: 'text',
2033
+ text: JSON.stringify({
2034
+ status: 'error',
2035
+ error: error instanceof Error ? error.message : 'Verification failed',
2036
+ }, null, 2)
2037
+ }],
2038
+ isError: true
2039
+ };
2040
+ }
2041
+ }
1897
2042
 
1898
2043
  case 'check_workspace_status': {
1899
2044
  // Check if we have a local session