@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/convex/schema.ts CHANGED
@@ -364,6 +364,19 @@ export default defineSchema({
364
364
  .index("by_workspaceId", ["workspaceId"])
365
365
  .index("by_status", ["status"]),
366
366
 
367
+ // OTP codes for terminal-native email verification
368
+ otpCodes: defineTable({
369
+ email: v.string(),
370
+ code: v.string(), // 6-digit code
371
+ fingerprint: v.optional(v.string()),
372
+ expiresAt: v.number(),
373
+ usedAt: v.optional(v.number()),
374
+ attempts: v.number(), // failed attempts counter
375
+ createdAt: v.number(),
376
+ })
377
+ .index("by_email", ["email"])
378
+ .index("by_email_code", ["email", "code"]),
379
+
367
380
  // Magic link tokens for email auth
368
381
  magicLinks: defineTable({
369
382
  email: v.string(),
@@ -2,6 +2,191 @@ import { mutation, query } from "./_generated/server";
2
2
  import { internal } from "./_generated/api";
3
3
  import { v } from "convex/values";
4
4
 
5
+ // ============================================
6
+ // OTP AUTH FOR WORKSPACES (terminal-native)
7
+ // ============================================
8
+
9
+ function generateOTP(): string {
10
+ const digits = "0123456789";
11
+ let code = "";
12
+ for (let i = 0; i < 6; i++) {
13
+ code += digits.charAt(Math.floor(Math.random() * digits.length));
14
+ }
15
+ return code;
16
+ }
17
+
18
+ // Create OTP code and return it (MCP server sends the email)
19
+ export const createOTP = mutation({
20
+ args: {
21
+ email: v.string(),
22
+ fingerprint: v.optional(v.string()),
23
+ },
24
+ handler: async (ctx, { email, fingerprint }) => {
25
+ const normalizedEmail = email.toLowerCase().trim();
26
+
27
+ // Invalidate any existing unused OTPs for this email
28
+ const existing = await ctx.db
29
+ .query("otpCodes")
30
+ .withIndex("by_email", (q) => q.eq("email", normalizedEmail))
31
+ .collect();
32
+ for (const otp of existing) {
33
+ if (!otp.usedAt && otp.expiresAt > Date.now()) {
34
+ await ctx.db.patch(otp._id, { expiresAt: 0 }); // expire it
35
+ }
36
+ }
37
+
38
+ const code = generateOTP();
39
+ const expiresAt = Date.now() + 10 * 60 * 1000; // 10 minutes
40
+
41
+ await ctx.db.insert("otpCodes", {
42
+ email: normalizedEmail,
43
+ code,
44
+ fingerprint,
45
+ expiresAt,
46
+ usedAt: undefined,
47
+ attempts: 0,
48
+ createdAt: Date.now(),
49
+ });
50
+
51
+ return { code, expiresAt };
52
+ },
53
+ });
54
+
55
+ // Verify OTP code, create/activate workspace, return session
56
+ export const verifyOTP = mutation({
57
+ args: {
58
+ email: v.string(),
59
+ code: v.string(),
60
+ fingerprint: v.optional(v.string()),
61
+ },
62
+ handler: async (ctx, { email, code, fingerprint }) => {
63
+ const normalizedEmail = email.toLowerCase().trim();
64
+
65
+ const otpRecord = await ctx.db
66
+ .query("otpCodes")
67
+ .withIndex("by_email_code", (q) =>
68
+ q.eq("email", normalizedEmail).eq("code", code)
69
+ )
70
+ .first();
71
+
72
+ if (!otpRecord) {
73
+ return { success: false, error: "invalid_code", message: "Invalid verification code." };
74
+ }
75
+
76
+ if (otpRecord.usedAt) {
77
+ return { success: false, error: "code_used", message: "Code already used." };
78
+ }
79
+
80
+ if (otpRecord.expiresAt < Date.now()) {
81
+ return { success: false, error: "code_expired", message: "Code expired. Run register_owner again to get a new code." };
82
+ }
83
+
84
+ if (otpRecord.attempts >= 5) {
85
+ return { success: false, error: "too_many_attempts", message: "Too many failed attempts. Run register_owner again." };
86
+ }
87
+
88
+ // Mark OTP as used
89
+ await ctx.db.patch(otpRecord._id, { usedAt: Date.now() });
90
+
91
+ // Find or create workspace
92
+ let workspace = await ctx.db
93
+ .query("workspaces")
94
+ .withIndex("by_email", (q) => q.eq("email", normalizedEmail))
95
+ .first();
96
+
97
+ let isNewUser = false;
98
+ if (!workspace) {
99
+ isNewUser = true;
100
+ // Generate referral code
101
+ let referralCode: string;
102
+ let attempts = 0;
103
+ do {
104
+ const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
105
+ let rc = "";
106
+ for (let i = 0; i < 6; i++) {
107
+ rc += chars.charAt(Math.floor(Math.random() * chars.length));
108
+ }
109
+ referralCode = `CLAW-${rc}`;
110
+ const existingRef = await ctx.db
111
+ .query("workspaces")
112
+ .withIndex("by_referralCode", (q) => q.eq("referralCode", referralCode))
113
+ .first();
114
+ if (!existingRef) break;
115
+ attempts++;
116
+ } while (attempts < 10);
117
+
118
+ const workspaceId = await ctx.db.insert("workspaces", {
119
+ email: normalizedEmail,
120
+ status: "active",
121
+ tier: "free",
122
+ usageCount: 0,
123
+ usageLimit: 50,
124
+ weeklyUsageCount: 0,
125
+ weeklyUsageLimit: 50,
126
+ lastWeeklyResetAt: Date.now(),
127
+ hourlyUsageCount: 0,
128
+ lastHourlyResetAt: Date.now(),
129
+ referralCode: referralCode!,
130
+ createdAt: Date.now(),
131
+ updatedAt: Date.now(),
132
+ });
133
+ workspace = await ctx.db.get(workspaceId);
134
+ } else if (workspace.status === "pending") {
135
+ // Activate pending workspace
136
+ await ctx.db.patch(workspace._id, { status: "active" });
137
+ workspace = await ctx.db.get(workspace._id);
138
+ }
139
+
140
+ if (!workspace) {
141
+ return { success: false, error: "workspace_error", message: "Failed to create workspace." };
142
+ }
143
+
144
+ // Create agent session
145
+ const sessionToken = generateToken();
146
+ await ctx.db.insert("agentSessions", {
147
+ workspaceId: workspace._id,
148
+ sessionToken,
149
+ fingerprint: fingerprint || "unknown",
150
+ lastUsedAt: Date.now(),
151
+ createdAt: Date.now(),
152
+ });
153
+
154
+ return {
155
+ success: true,
156
+ isNewUser,
157
+ sessionToken,
158
+ workspace: {
159
+ id: workspace._id,
160
+ email: workspace.email,
161
+ tier: workspace.tier,
162
+ status: "active",
163
+ usageCount: workspace.usageCount,
164
+ usageLimit: workspace.usageLimit,
165
+ },
166
+ };
167
+ },
168
+ });
169
+
170
+ // Increment failed OTP attempt counter
171
+ export const incrementOTPAttempt = mutation({
172
+ args: {
173
+ email: v.string(),
174
+ code: v.string(),
175
+ },
176
+ handler: async (ctx, { email, code }) => {
177
+ const normalizedEmail = email.toLowerCase().trim();
178
+ const otpRecord = await ctx.db
179
+ .query("otpCodes")
180
+ .withIndex("by_email_code", (q) =>
181
+ q.eq("email", normalizedEmail).eq("code", code)
182
+ )
183
+ .first();
184
+ if (otpRecord && !otpRecord.usedAt) {
185
+ await ctx.db.patch(otpRecord._id, { attempts: otpRecord.attempts + 1 });
186
+ }
187
+ },
188
+ });
189
+
5
190
  // ============================================
6
191
  // MAGIC LINK AUTH FOR WORKSPACES
7
192
  // ============================================
package/dist/index.js CHANGED
@@ -21,7 +21,6 @@ import { executeMetered } from './metered.js';
21
21
  import { logAPICall } from './mcp-analytics.js';
22
22
  import { isOpenAPI, executeOpenAPI, listOpenAPIs, getOpenAPIBaseUrl } from './open-apis.js';
23
23
  import { getGateway, isGatewayEnabled } from './gateway-client.js';
24
- import { PROXY_PROVIDERS } from './proxy.js';
25
24
  import { requiresConfirmationAsync, createPendingAction, consumePendingAction, generatePreview, validateParams } from './confirmation.js';
26
25
  import { executeCapability, listCapabilities, hasCapability } from './capability-router.js';
27
26
  import { readSession, writeSession, clearSession, getMachineFingerprint, detectMCPClient } from './session.js';
@@ -36,6 +35,7 @@ const CONVEX_URL = process.env.CONVEX_URL || 'https://adventurous-avocet-799.con
36
35
  const convex = new ConvexHttpClient(CONVEX_URL);
37
36
  let workspaceContext = null;
38
37
  let currentAgentId = null; // Agent ID from agents table (set on startup)
38
+ let pendingRegistrationEmail = null; // Email waiting for OTP verification
39
39
  const anonymousRateLimits = new Map();
40
40
  // Rate limit constants
41
41
  const ANONYMOUS_HOURLY_LIMIT = 5;
@@ -188,32 +188,21 @@ const rateLimitStore = new Map();
188
188
  // Unregistered (auto-provisioned, no email) users get this many calls before signup required
189
189
  const UNREGISTERED_CALL_LIMIT = 5;
190
190
  /**
191
- * For proxy providers, allow anonymous usage with rate limiting
191
+ * Check workspace access -- registration required for all API calls
192
192
  */
193
193
  function checkWorkspaceAccess(providerId) {
194
- // Allow anonymous access for proxy providers
195
- if (providerId && PROXY_PROVIDERS.includes(providerId)) {
196
- if (!workspaceContext) {
197
- // Anonymous user - check rate limits
198
- const fingerprint = getMachineFingerprint();
199
- const rateLimitCheck = checkAnonymousRateLimit(fingerprint);
200
- if (!rateLimitCheck.allowed) {
201
- return {
202
- allowed: false,
203
- error: rateLimitCheck.error,
204
- isAnonymous: true
205
- };
206
- }
207
- return { allowed: true, isAnonymous: true };
208
- }
209
- // Authenticated user using proxy provider - allow with higher limits
210
- return { allowed: true, isAnonymous: false };
211
- }
212
- // Non-proxy providers require authentication
194
+ // All API calls require registration now
213
195
  if (!workspaceContext) {
214
196
  return {
215
197
  allowed: false,
216
- error: 'Not authenticated. Use register_owner to authenticate your workspace.'
198
+ error: JSON.stringify({
199
+ status: 'registration_required',
200
+ error: 'Registration required to call APIs.',
201
+ 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.',
202
+ action: 'register_owner',
203
+ free_tier: '50 API calls/month -- completely free.',
204
+ }, null, 2),
205
+ isAnonymous: true,
217
206
  };
218
207
  }
219
208
  if (workspaceContext.status !== 'active') {
@@ -408,7 +397,7 @@ const tools = [
408
397
  },
409
398
  {
410
399
  name: 'call_api',
411
- description: `Execute an API call through APIClaw. Supports single calls AND multi-step chains.
400
+ description: `Execute an API call through APIClaw. Requires registration (free). If not registered, call register_owner first.
412
401
 
413
402
  SINGLE CALL: Provide provider + action + params
414
403
  CHAIN: Provide chain array to execute multiple APIs in sequence/parallel with cross-step references.
@@ -580,7 +569,7 @@ Example chain:
580
569
  // ============================================
581
570
  {
582
571
  name: 'register_owner',
583
- description: 'Register your email to create a workspace. This authenticates your agent with APIClaw. You will receive a magic link to verify ownership.',
572
+ 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.',
584
573
  inputSchema: {
585
574
  type: 'object',
586
575
  properties: {
@@ -592,6 +581,24 @@ Example chain:
592
581
  required: ['email']
593
582
  }
594
583
  },
584
+ {
585
+ name: 'verify_code',
586
+ 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.',
587
+ inputSchema: {
588
+ type: 'object',
589
+ properties: {
590
+ email: {
591
+ type: 'string',
592
+ description: 'The email address used in register_owner'
593
+ },
594
+ code: {
595
+ type: 'string',
596
+ description: 'The 6-digit verification code from the email'
597
+ }
598
+ },
599
+ required: ['email', 'code']
600
+ }
601
+ },
595
602
  {
596
603
  name: 'check_workspace_status',
597
604
  description: 'Check your workspace status, tier, and usage remaining.',
@@ -721,37 +728,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
721
728
  try {
722
729
  switch (name) {
723
730
  case 'apiclaw_help': {
731
+ const isAuthenticated = !!workspaceContext;
724
732
  const helpText = `
725
- 🦞 APIClaw The API Layer for AI Agents
733
+ 🦞 APIClaw -- The API Layer for AI Agents
726
734
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
727
-
728
- DISCOVER APIs:
735
+ ${!isAuthenticated ? `
736
+ GET STARTED (free):
737
+ 1. register_owner({ email: "you@example.com" }) — sends 6-digit code
738
+ 2. verify_code({ email: "you@example.com", code: "123456" }) — activates workspace
739
+ ` : `
740
+ STATUS: Authenticated as ${workspaceContext.email} (${workspaceContext.tier} tier)
741
+ `}
742
+ DISCOVER APIs (free, no registration needed):
729
743
  discover_apis({ query: "send SMS to Sweden" })
730
- discover_apis({ query: "search the web", max_results: 10 })
731
744
  discover_apis({ query: "text to speech", category: "ai" })
732
745
 
733
- GET DETAILS:
734
- get_api_details({ api_id: "46elks" })
735
-
736
- DIRECT CALL (8 APIs, no key needed):
737
- get_connected_providers()
738
- call_api({ provider: "brave_search", endpoint: "search", params: { query: "AI agents" } })
746
+ CALL APIs (requires free registration):
747
+ call_api({ provider: "brave_search", action: "search", params: { q: "AI agents" } })
748
+ call_api({ provider: "elevenlabs", action: "tts", params: { text: "Hello" } })
739
749
 
740
- Available direct-call providers:
741
- brave_search Web search
742
- 46elks SMS (Sweden)
743
- twilio SMS (Global)
744
- resend Email
745
- • openrouter — LLM routing (100+ models)
746
- • elevenlabs — Text-to-speech
747
- • replicate — AI models (images, video, audio)
748
- • firecrawl — Web scraping & crawling
749
- • github — Code repos & developer data
750
- • e2b — Code sandbox for AI agents
750
+ 23 MANAGED PROVIDERS:
751
+ OpenAI, Anthropic, xAI/Grok, Groq, Mistral, OpenRouter, Together AI,
752
+ Replicate, ElevenLabs, Deepgram, AssemblyAI, Brave Search, Firecrawl,
753
+ Serper, Resend, 46elks, Twilio, E2B, Stability AI, Cohere, Voyage AI,
754
+ GitHub, APILayer (27 sub-APIs)
751
755
 
752
- BROWSE:
753
- list_categories()
754
- list_all_apis({ category: "communication", limit: 20 })
756
+ 26,700+ DISCOVERABLE | 1,654 CALLABLE | Free tier: 50 calls/month
755
757
 
756
758
  Docs: https://apiclaw.cloud
757
759
  `;
@@ -1043,6 +1045,24 @@ Docs: https://apiclaw.cloud
1043
1045
  };
1044
1046
  }
1045
1047
  case 'call_api': {
1048
+ // ============================================
1049
+ // REGISTRATION GATE: require email before any API call
1050
+ // ============================================
1051
+ if (!workspaceContext) {
1052
+ return {
1053
+ content: [{
1054
+ type: 'text',
1055
+ text: JSON.stringify({
1056
+ status: 'registration_required',
1057
+ error: 'You need to register before making API calls.',
1058
+ 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.',
1059
+ action: 'register_owner',
1060
+ free_tier: '50 API calls/month, unlimited discovery -- completely free.',
1061
+ }, null, 2)
1062
+ }],
1063
+ isError: true
1064
+ };
1065
+ }
1046
1066
  const provider = args?.provider;
1047
1067
  const action = args?.action;
1048
1068
  const params = args?.params || {};
@@ -1474,6 +1494,21 @@ Docs: https://apiclaw.cloud
1474
1494
  };
1475
1495
  }
1476
1496
  case 'capability': {
1497
+ // Registration gate
1498
+ if (!workspaceContext) {
1499
+ return {
1500
+ content: [{
1501
+ type: 'text',
1502
+ text: JSON.stringify({
1503
+ status: 'registration_required',
1504
+ error: 'You need to register before making API calls.',
1505
+ 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.',
1506
+ action: 'register_owner',
1507
+ }, null, 2)
1508
+ }],
1509
+ isError: true
1510
+ };
1511
+ }
1477
1512
  const capabilityId = args?.capability;
1478
1513
  const action = args?.action;
1479
1514
  const params = args?.params || {};
@@ -1565,10 +1600,9 @@ Docs: https://apiclaw.cloud
1565
1600
  };
1566
1601
  }
1567
1602
  try {
1568
- // Check if workspace already exists
1603
+ // Check if workspace already exists and is active -- auto-login
1569
1604
  const existing = await convex.query("workspaces:getByEmail", { email });
1570
1605
  if (existing && existing.status === 'active') {
1571
- // Workspace exists and is active - create session directly
1572
1606
  const fingerprint = getMachineFingerprint();
1573
1607
  const sessionResult = await convex.mutation("workspaces:createAgentSession", {
1574
1608
  workspaceId: existing.id,
@@ -1576,21 +1610,16 @@ Docs: https://apiclaw.cloud
1576
1610
  });
1577
1611
  if (sessionResult.success) {
1578
1612
  writeSession(sessionResult.sessionToken, existing.id, email);
1579
- // Claim anonymous usage history
1580
1613
  try {
1581
1614
  const claimResult = await convex.mutation("workspaces:claimAnonymousUsage", {
1582
1615
  workspaceId: existing.id,
1583
1616
  machineFingerprint: fingerprint,
1584
1617
  });
1585
1618
  if (claimResult.success && claimResult.claimedCount) {
1586
- console.error(`[APIClaw] Claimed ${claimResult.claimedCount} anonymous usage records`);
1619
+ console.error(`[APIClaw] Claimed ${claimResult.claimedCount} anonymous usage records`);
1587
1620
  }
1588
1621
  }
1589
- catch (err) {
1590
- // Non-critical error, just log it
1591
- console.error('[APIClaw] Warning: Failed to claim anonymous usage:', err);
1592
- }
1593
- // Update global context
1622
+ catch (_) { }
1594
1623
  workspaceContext = {
1595
1624
  sessionToken: sessionResult.sessionToken,
1596
1625
  workspaceId: existing.id,
@@ -1617,26 +1646,13 @@ Docs: https://apiclaw.cloud
1617
1646
  };
1618
1647
  }
1619
1648
  }
1620
- // Create workspace and magic link
1621
- const createResult = await convex.mutation("workspaces:createWorkspace", { email });
1622
- let workspaceId;
1623
- if (createResult.success) {
1624
- workspaceId = createResult.workspaceId;
1625
- }
1626
- else if (createResult.error === 'workspace_exists') {
1627
- workspaceId = createResult.workspaceId;
1628
- }
1629
- else {
1630
- throw new Error(createResult.error);
1631
- }
1632
- // Create magic link
1649
+ // New user or pending workspace -- send OTP
1633
1650
  const fingerprint = getMachineFingerprint();
1634
- const magicLinkResult = await convex.mutation("workspaces:createMagicLink", {
1651
+ const otpResult = await convex.mutation("workspaces:createOTP", {
1635
1652
  email,
1636
1653
  fingerprint,
1637
1654
  });
1638
- // Send magic link via email
1639
- const verifyUrl = `https://apiclaw.cloud/auth/verify?token=${magicLinkResult.token}`;
1655
+ // Send OTP email
1640
1656
  const emailResponse = await fetch('https://api.resend.com/emails', {
1641
1657
  method: 'POST',
1642
1658
  headers: {
@@ -1646,23 +1662,39 @@ Docs: https://apiclaw.cloud
1646
1662
  body: JSON.stringify({
1647
1663
  from: 'APIClaw <noreply@apiclaw.cloud>',
1648
1664
  to: email,
1649
- subject: 'Verify your APIClaw workspace',
1650
- html: `<p>Click to verify: <a href="${verifyUrl}">${verifyUrl}</a></p><p>Expires in 15 minutes.</p>`
1665
+ subject: `Your APIClaw verification code: ${otpResult.code}`,
1666
+ html: `
1667
+ <div style="font-family: Inter, sans-serif; max-width: 480px; margin: 0 auto; padding: 40px 24px;">
1668
+ <div style="text-align: center; margin-bottom: 32px;">
1669
+ <span style="font-size: 48px;">🦞</span>
1670
+ </div>
1671
+ <h1 style="font-size: 24px; font-weight: 700; color: #0A0A0A; text-align: center; margin-bottom: 8px;">Your verification code</h1>
1672
+ <p style="font-size: 16px; color: #525252; text-align: center; margin-bottom: 32px;">Paste this code in your terminal to activate APIClaw.</p>
1673
+ <div style="background: #F5F5F5; border: 1px solid #E5E5E5; border-radius: 12px; padding: 24px; text-align: center; margin-bottom: 24px;">
1674
+ <code style="font-size: 36px; font-weight: 700; letter-spacing: 0.3em; color: #EF4444; font-family: 'JetBrains Mono', monospace;">${otpResult.code}</code>
1675
+ </div>
1676
+ <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>
1677
+ <hr style="border: none; border-top: 1px solid #E5E5E5; margin: 32px 0 16px;" />
1678
+ <p style="font-size: 12px; color: #A3A3A3; text-align: center;">APIClaw -- The API Layer For AI Agents</p>
1679
+ </div>
1680
+ `
1651
1681
  })
1652
1682
  });
1653
1683
  if (!emailResponse.ok) {
1654
1684
  const errorData = await emailResponse.text();
1655
1685
  throw new Error(`Failed to send verification email: ${errorData}`);
1656
1686
  }
1687
+ // Store pending email for verify_code
1688
+ pendingRegistrationEmail = email;
1657
1689
  return {
1658
1690
  content: [{
1659
1691
  type: 'text',
1660
1692
  text: JSON.stringify({
1661
- status: 'pending_verification',
1662
- message: 'Workspace created! Check your email for verification link.',
1693
+ status: 'code_sent',
1694
+ message: `Verification code sent to ${email}`,
1695
+ next_step: 'Ask the user to check their email for a 6-digit code, then call verify_code with the email and code.',
1663
1696
  email,
1664
- expires_in_minutes: 15,
1665
- next_step: 'Check your email, click the verification link, then run check_workspace_status',
1697
+ expires_in_minutes: 10,
1666
1698
  }, null, 2)
1667
1699
  }]
1668
1700
  };
@@ -1680,6 +1712,105 @@ Docs: https://apiclaw.cloud
1680
1712
  };
1681
1713
  }
1682
1714
  }
1715
+ case 'verify_code': {
1716
+ const email = args?.email || pendingRegistrationEmail;
1717
+ const code = args?.code;
1718
+ if (!email || !code) {
1719
+ return {
1720
+ content: [{
1721
+ type: 'text',
1722
+ text: JSON.stringify({
1723
+ status: 'error',
1724
+ error: 'Both email and code are required.',
1725
+ hint: 'Call register_owner first to receive a verification code.',
1726
+ }, null, 2)
1727
+ }],
1728
+ isError: true
1729
+ };
1730
+ }
1731
+ try {
1732
+ const fingerprint = getMachineFingerprint();
1733
+ const result = await convex.mutation("workspaces:verifyOTP", {
1734
+ email,
1735
+ code: code.trim(),
1736
+ fingerprint,
1737
+ });
1738
+ if (!result.success) {
1739
+ // Increment attempt counter
1740
+ try {
1741
+ await convex.mutation("workspaces:incrementOTPAttempt", { email, code: code.trim() });
1742
+ }
1743
+ catch (_) { }
1744
+ return {
1745
+ content: [{
1746
+ type: 'text',
1747
+ text: JSON.stringify({
1748
+ status: 'error',
1749
+ error: result.message || 'Verification failed',
1750
+ hint: result.error === 'code_expired'
1751
+ ? 'Run register_owner again to get a new code.'
1752
+ : 'Check the code and try again.',
1753
+ }, null, 2)
1754
+ }],
1755
+ isError: true
1756
+ };
1757
+ }
1758
+ // Success! Save session
1759
+ writeSession(result.sessionToken, result.workspace.id, result.workspace.email);
1760
+ // Claim anonymous usage
1761
+ try {
1762
+ const claimResult = await convex.mutation("workspaces:claimAnonymousUsage", {
1763
+ workspaceId: result.workspace.id,
1764
+ machineFingerprint: fingerprint,
1765
+ });
1766
+ if (claimResult.success && claimResult.claimedCount) {
1767
+ console.error(`[APIClaw] Claimed ${claimResult.claimedCount} anonymous usage records`);
1768
+ }
1769
+ }
1770
+ catch (_) { }
1771
+ // Update global context
1772
+ workspaceContext = {
1773
+ sessionToken: result.sessionToken,
1774
+ workspaceId: result.workspace.id,
1775
+ email: result.workspace.email,
1776
+ tier: result.workspace.tier,
1777
+ usageRemaining: result.workspace.usageLimit - result.workspace.usageCount,
1778
+ usageCount: result.workspace.usageCount,
1779
+ status: result.workspace.status,
1780
+ };
1781
+ pendingRegistrationEmail = null;
1782
+ return {
1783
+ content: [{
1784
+ type: 'text',
1785
+ text: JSON.stringify({
1786
+ status: 'success',
1787
+ message: result.isNewUser
1788
+ ? `Welcome to APIClaw! Workspace activated for ${result.workspace.email}`
1789
+ : `Welcome back! Authenticated as ${result.workspace.email}`,
1790
+ workspace: {
1791
+ email: result.workspace.email,
1792
+ tier: result.workspace.tier,
1793
+ usageCount: result.workspace.usageCount,
1794
+ usageLimit: result.workspace.usageLimit,
1795
+ },
1796
+ ready: 'You can now use discover_apis and call_api.',
1797
+ }, null, 2)
1798
+ }]
1799
+ };
1800
+ }
1801
+ catch (error) {
1802
+ return {
1803
+ content: [{
1804
+ type: 'text',
1805
+ text: JSON.stringify({
1806
+ status: 'error',
1807
+ error: error instanceof Error ? error.message : 'Verification failed',
1808
+ }, null, 2)
1809
+ }],
1810
+ isError: true
1811
+ };
1812
+ }
1813
+ }
1683
1814
  case 'check_workspace_status': {
1684
1815
  // Check if we have a local session
1685
1816
  const session = readSession();