@nordsym/apiclaw 2.0.0 → 2.2.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/dist/index.js CHANGED
@@ -21,10 +21,11 @@ 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';
27
+ import { requireVerifiedOwner } from './registration-guard.js';
28
+ import { emitFunnelEvent, hasLocalMarker, setLocalMarker } from './funnel-client.js';
28
29
  import { ConvexHttpClient } from 'convex/browser';
29
30
  import { getOrCreateCustomer, createMeteredCheckoutSession, getUsageSummary, METERED_BILLING } from './stripe.js';
30
31
  import { estimateCost } from './metered.js';
@@ -36,6 +37,7 @@ const CONVEX_URL = process.env.CONVEX_URL || 'https://adventurous-avocet-799.con
36
37
  const convex = new ConvexHttpClient(CONVEX_URL);
37
38
  let workspaceContext = null;
38
39
  let currentAgentId = null; // Agent ID from agents table (set on startup)
40
+ let pendingRegistrationEmail = null; // Email waiting for OTP verification
39
41
  const anonymousRateLimits = new Map();
40
42
  // Rate limit constants
41
43
  const ANONYMOUS_HOURLY_LIMIT = 5;
@@ -188,32 +190,21 @@ const rateLimitStore = new Map();
188
190
  // Unregistered (auto-provisioned, no email) users get this many calls before signup required
189
191
  const UNREGISTERED_CALL_LIMIT = 5;
190
192
  /**
191
- * For proxy providers, allow anonymous usage with rate limiting
193
+ * Check workspace access -- registration required for all API calls
192
194
  */
193
195
  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
196
+ // All API calls require registration now
213
197
  if (!workspaceContext) {
214
198
  return {
215
199
  allowed: false,
216
- error: 'Not authenticated. Use register_owner to authenticate your workspace.'
200
+ error: JSON.stringify({
201
+ status: 'registration_required',
202
+ error: 'Registration required to call APIs.',
203
+ 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.',
204
+ action: 'register_owner',
205
+ free_tier: '50 API calls/month -- completely free.',
206
+ }, null, 2),
207
+ isAnonymous: true,
217
208
  };
218
209
  }
219
210
  if (workspaceContext.status !== 'active') {
@@ -256,6 +247,49 @@ function checkWorkspaceAccess(providerId) {
256
247
  }
257
248
  return { allowed: true, isAnonymous: false };
258
249
  }
250
+ /**
251
+ * Single enforcement entry point for every paying call path.
252
+ * Returns either a verified workspace context or an MCP-formatted block response.
253
+ */
254
+ function enforceOwner(channel) {
255
+ const result = requireVerifiedOwner(workspaceContext);
256
+ if (result.ok) {
257
+ return { ok: true, ctx: result.ctx };
258
+ }
259
+ // Diagnostic: record why the call was blocked.
260
+ try {
261
+ emitFunnelEvent({
262
+ event: 'call_api_blocked',
263
+ workspaceId: workspaceContext?.workspaceId,
264
+ email: workspaceContext?.email,
265
+ fingerprint: getMachineFingerprint(),
266
+ mcpClient: detectMCPClient(),
267
+ platform: process.platform,
268
+ version: process.env.npm_package_version || 'unknown',
269
+ props: { reason: result.reason, channel },
270
+ });
271
+ if (result.reason === 'quota_exceeded') {
272
+ emitFunnelEvent({
273
+ event: 'quota_hit',
274
+ workspaceId: workspaceContext?.workspaceId,
275
+ email: workspaceContext?.email,
276
+ fingerprint: getMachineFingerprint(),
277
+ version: process.env.npm_package_version || 'unknown',
278
+ props: { tier: workspaceContext?.tier, limit: workspaceContext?.usageCount },
279
+ });
280
+ }
281
+ }
282
+ catch { /* non-blocking */ }
283
+ return {
284
+ ok: false,
285
+ response: {
286
+ content: [{ type: 'text', text: JSON.stringify(result.payload, null, 2) }],
287
+ isError: true,
288
+ },
289
+ };
290
+ }
291
+ // Per-process marker: ensure first_call_api_success fires once per server boot.
292
+ let firstCallEmitted = false;
259
293
  /**
260
294
  * Get customer API key from environment variable
261
295
  * Convention: {PROVIDER}_API_KEY (e.g., COACCEPT_API_KEY, ELKS_API_KEY)
@@ -408,7 +442,7 @@ const tools = [
408
442
  },
409
443
  {
410
444
  name: 'call_api',
411
- description: `Execute an API call through APIClaw. Supports single calls AND multi-step chains.
445
+ description: `Execute an API call through APIClaw. Requires registration (free). If not registered, call register_owner first.
412
446
 
413
447
  SINGLE CALL: Provide provider + action + params
414
448
  CHAIN: Provide chain array to execute multiple APIs in sequence/parallel with cross-step references.
@@ -580,7 +614,7 @@ Example chain:
580
614
  // ============================================
581
615
  {
582
616
  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.',
617
+ 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
618
  inputSchema: {
585
619
  type: 'object',
586
620
  properties: {
@@ -592,6 +626,24 @@ Example chain:
592
626
  required: ['email']
593
627
  }
594
628
  },
629
+ {
630
+ name: 'verify_code',
631
+ 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.',
632
+ inputSchema: {
633
+ type: 'object',
634
+ properties: {
635
+ email: {
636
+ type: 'string',
637
+ description: 'The email address used in register_owner'
638
+ },
639
+ code: {
640
+ type: 'string',
641
+ description: 'The 6-digit verification code from the email'
642
+ }
643
+ },
644
+ required: ['email', 'code']
645
+ }
646
+ },
595
647
  {
596
648
  name: 'check_workspace_status',
597
649
  description: 'Check your workspace status, tier, and usage remaining.',
@@ -721,37 +773,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
721
773
  try {
722
774
  switch (name) {
723
775
  case 'apiclaw_help': {
776
+ const isAuthenticated = !!workspaceContext;
724
777
  const helpText = `
725
- 🦞 APIClaw The API Layer for AI Agents
778
+ 🦞 APIClaw -- The API Layer for AI Agents
726
779
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
727
-
728
- DISCOVER APIs:
780
+ ${!isAuthenticated ? `
781
+ GET STARTED (free):
782
+ 1. register_owner({ email: "you@example.com" }) — sends 6-digit code
783
+ 2. verify_code({ email: "you@example.com", code: "123456" }) — activates workspace
784
+ ` : `
785
+ STATUS: Authenticated as ${workspaceContext.email} (${workspaceContext.tier} tier)
786
+ `}
787
+ DISCOVER APIs (free, no registration needed):
729
788
  discover_apis({ query: "send SMS to Sweden" })
730
- discover_apis({ query: "search the web", max_results: 10 })
731
789
  discover_apis({ query: "text to speech", category: "ai" })
732
790
 
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" } })
791
+ CALL APIs (requires free registration):
792
+ call_api({ provider: "brave_search", action: "search", params: { q: "AI agents" } })
793
+ call_api({ provider: "elevenlabs", action: "tts", params: { text: "Hello" } })
739
794
 
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
795
+ 23 MANAGED PROVIDERS:
796
+ OpenAI, Anthropic, xAI/Grok, Groq, Mistral, OpenRouter, Together AI,
797
+ Replicate, ElevenLabs, Deepgram, AssemblyAI, Brave Search, Firecrawl,
798
+ Serper, Resend, 46elks, Twilio, E2B, Stability AI, Cohere, Voyage AI,
799
+ GitHub, APILayer (27 sub-APIs)
751
800
 
752
- BROWSE:
753
- list_categories()
754
- list_all_apis({ category: "communication", limit: 20 })
801
+ 26,700+ DISCOVERABLE | 1,654 CALLABLE | Free tier: 50 calls/month
755
802
 
756
803
  Docs: https://apiclaw.cloud
757
804
  `;
@@ -1043,6 +1090,12 @@ Docs: https://apiclaw.cloud
1043
1090
  };
1044
1091
  }
1045
1092
  case 'call_api': {
1093
+ // ============================================
1094
+ // REGISTRATION GATE: requireVerifiedOwner (single source of truth)
1095
+ // ============================================
1096
+ const gate = enforceOwner("mcp:call_api");
1097
+ if (!gate.ok)
1098
+ return gate.response;
1046
1099
  const provider = args?.provider;
1047
1100
  const action = args?.action;
1048
1101
  const params = args?.params || {};
@@ -1070,32 +1123,7 @@ Docs: https://apiclaw.cloud
1070
1123
  // CHAIN EXECUTION MODE
1071
1124
  // ============================================
1072
1125
  if (chain && Array.isArray(chain) && chain.length > 0) {
1073
- // Check workspace access for chains
1074
- const access = checkWorkspaceAccess();
1075
- if (!access.allowed) {
1076
- // If error is already formatted JSON (from rate limit checks), return as-is
1077
- if (access.error?.startsWith('{')) {
1078
- return {
1079
- content: [{
1080
- type: 'text',
1081
- text: access.error
1082
- }],
1083
- isError: true
1084
- };
1085
- }
1086
- // Otherwise, wrap in standard error format
1087
- return {
1088
- content: [{
1089
- type: 'text',
1090
- text: JSON.stringify({
1091
- status: 'error',
1092
- error: access.error,
1093
- hint: 'Use register_owner to authenticate your workspace.',
1094
- }, null, 2)
1095
- }],
1096
- isError: true
1097
- };
1098
- }
1126
+ // Gate already enforced at top of call_api via enforceOwner().
1099
1127
  try {
1100
1128
  // Construct ChainDefinition from the input
1101
1129
  const chainDefinition = {
@@ -1423,6 +1451,37 @@ Docs: https://apiclaw.cloud
1423
1451
  if (isGatewayEnabled() && result.success && workspaceContext && !isFreeAPI) {
1424
1452
  workspaceContext.usageCount = (workspaceContext.usageCount || 0) + 1;
1425
1453
  }
1454
+ // Funnel: call_api_error (provider-level failure)
1455
+ if (!result.success && workspaceContext) {
1456
+ emitFunnelEvent({
1457
+ event: 'call_api_error',
1458
+ workspaceId: workspaceContext.workspaceId,
1459
+ email: workspaceContext.email,
1460
+ fingerprint: getMachineFingerprint(),
1461
+ version: process.env.npm_package_version || 'unknown',
1462
+ props: {
1463
+ provider: result.provider || provider,
1464
+ action: result.action || action,
1465
+ errorCode: (result.error || '').slice(0, 80) || 'unknown',
1466
+ },
1467
+ });
1468
+ }
1469
+ // Funnel: first_call_api_success (once per workspace, deduped server-side)
1470
+ if (result.success && workspaceContext && !isFreeAPI && !firstCallEmitted) {
1471
+ firstCallEmitted = true;
1472
+ emitFunnelEvent({
1473
+ event: 'first_call_api_success',
1474
+ email: workspaceContext.email,
1475
+ workspaceId: workspaceContext.workspaceId,
1476
+ sessionToken: workspaceContext.sessionToken,
1477
+ fingerprint: getMachineFingerprint(),
1478
+ mcpClient: detectMCPClient(),
1479
+ platform: process.platform,
1480
+ version: process.env.npm_package_version || 'unknown',
1481
+ dedupeKey: `first_call:${workspaceContext.workspaceId}`,
1482
+ props: { provider, action, channel: 'mcp:call_api' },
1483
+ });
1484
+ }
1426
1485
  // Build response with signup nudge for unregistered users
1427
1486
  const responseData = {
1428
1487
  status: result.success ? 'success' : 'error',
@@ -1474,6 +1533,10 @@ Docs: https://apiclaw.cloud
1474
1533
  };
1475
1534
  }
1476
1535
  case 'capability': {
1536
+ // Registration gate: requireVerifiedOwner (single source of truth)
1537
+ const capGate = enforceOwner("mcp:capability");
1538
+ if (!capGate.ok)
1539
+ return capGate.response;
1477
1540
  const capabilityId = args?.capability;
1478
1541
  const action = args?.action;
1479
1542
  const params = args?.params || {};
@@ -1553,6 +1616,14 @@ Docs: https://apiclaw.cloud
1553
1616
  case 'register_owner': {
1554
1617
  const email = args?.email;
1555
1618
  if (!email || !email.includes('@')) {
1619
+ emitFunnelEvent({
1620
+ event: 'register_owner_failed',
1621
+ email,
1622
+ fingerprint: getMachineFingerprint(),
1623
+ mcpClient: detectMCPClient(),
1624
+ version: process.env.npm_package_version || 'unknown',
1625
+ props: { reason: 'invalid_email' },
1626
+ });
1556
1627
  return {
1557
1628
  content: [{
1558
1629
  type: 'text',
@@ -1565,10 +1636,9 @@ Docs: https://apiclaw.cloud
1565
1636
  };
1566
1637
  }
1567
1638
  try {
1568
- // Check if workspace already exists
1639
+ // Check if workspace already exists and is active -- auto-login
1569
1640
  const existing = await convex.query("workspaces:getByEmail", { email });
1570
1641
  if (existing && existing.status === 'active') {
1571
- // Workspace exists and is active - create session directly
1572
1642
  const fingerprint = getMachineFingerprint();
1573
1643
  const sessionResult = await convex.mutation("workspaces:createAgentSession", {
1574
1644
  workspaceId: existing.id,
@@ -1576,21 +1646,16 @@ Docs: https://apiclaw.cloud
1576
1646
  });
1577
1647
  if (sessionResult.success) {
1578
1648
  writeSession(sessionResult.sessionToken, existing.id, email);
1579
- // Claim anonymous usage history
1580
1649
  try {
1581
1650
  const claimResult = await convex.mutation("workspaces:claimAnonymousUsage", {
1582
1651
  workspaceId: existing.id,
1583
1652
  machineFingerprint: fingerprint,
1584
1653
  });
1585
1654
  if (claimResult.success && claimResult.claimedCount) {
1586
- console.error(`[APIClaw] Claimed ${claimResult.claimedCount} anonymous usage records`);
1655
+ console.error(`[APIClaw] Claimed ${claimResult.claimedCount} anonymous usage records`);
1587
1656
  }
1588
1657
  }
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
1658
+ catch (_) { }
1594
1659
  workspaceContext = {
1595
1660
  sessionToken: sessionResult.sessionToken,
1596
1661
  workspaceId: existing.id,
@@ -1617,26 +1682,13 @@ Docs: https://apiclaw.cloud
1617
1682
  };
1618
1683
  }
1619
1684
  }
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
1685
+ // New user or pending workspace -- send OTP
1633
1686
  const fingerprint = getMachineFingerprint();
1634
- const magicLinkResult = await convex.mutation("workspaces:createMagicLink", {
1687
+ const otpResult = await convex.mutation("workspaces:createOTP", {
1635
1688
  email,
1636
1689
  fingerprint,
1637
1690
  });
1638
- // Send magic link via email
1639
- const verifyUrl = `https://apiclaw.cloud/auth/verify?token=${magicLinkResult.token}`;
1691
+ // Send OTP email
1640
1692
  const emailResponse = await fetch('https://api.resend.com/emails', {
1641
1693
  method: 'POST',
1642
1694
  headers: {
@@ -1646,23 +1698,56 @@ Docs: https://apiclaw.cloud
1646
1698
  body: JSON.stringify({
1647
1699
  from: 'APIClaw <noreply@apiclaw.cloud>',
1648
1700
  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>`
1701
+ subject: `Your APIClaw verification code: ${otpResult.code}`,
1702
+ html: `
1703
+ <div style="font-family: Inter, sans-serif; max-width: 480px; margin: 0 auto; padding: 40px 24px;">
1704
+ <div style="text-align: center; margin-bottom: 32px;">
1705
+ <span style="font-size: 48px;">🦞</span>
1706
+ </div>
1707
+ <h1 style="font-size: 24px; font-weight: 700; color: #0A0A0A; text-align: center; margin-bottom: 8px;">Your verification code</h1>
1708
+ <p style="font-size: 16px; color: #525252; text-align: center; margin-bottom: 32px;">Paste this code in your terminal to activate APIClaw.</p>
1709
+ <div style="background: #F5F5F5; border: 1px solid #E5E5E5; border-radius: 12px; padding: 24px; text-align: center; margin-bottom: 24px;">
1710
+ <code style="font-size: 36px; font-weight: 700; letter-spacing: 0.3em; color: #EF4444; font-family: 'JetBrains Mono', monospace;">${otpResult.code}</code>
1711
+ </div>
1712
+ <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>
1713
+ <hr style="border: none; border-top: 1px solid #E5E5E5; margin: 32px 0 16px;" />
1714
+ <p style="font-size: 12px; color: #A3A3A3; text-align: center;">APIClaw -- The API Layer For AI Agents</p>
1715
+ </div>
1716
+ `
1651
1717
  })
1652
1718
  });
1653
1719
  if (!emailResponse.ok) {
1654
1720
  const errorData = await emailResponse.text();
1721
+ emitFunnelEvent({
1722
+ event: 'register_owner_failed',
1723
+ email,
1724
+ fingerprint: getMachineFingerprint(),
1725
+ mcpClient: detectMCPClient(),
1726
+ version: process.env.npm_package_version || 'unknown',
1727
+ props: { reason: 'email_send_failed' },
1728
+ });
1655
1729
  throw new Error(`Failed to send verification email: ${errorData}`);
1656
1730
  }
1731
+ // Store pending email for verify_code
1732
+ pendingRegistrationEmail = email;
1733
+ // Funnel: register_owner
1734
+ emitFunnelEvent({
1735
+ event: 'register_owner',
1736
+ email,
1737
+ fingerprint: getMachineFingerprint(),
1738
+ mcpClient: detectMCPClient(),
1739
+ platform: process.platform,
1740
+ version: process.env.npm_package_version || 'unknown',
1741
+ });
1657
1742
  return {
1658
1743
  content: [{
1659
1744
  type: 'text',
1660
1745
  text: JSON.stringify({
1661
- status: 'pending_verification',
1662
- message: 'Workspace created! Check your email for verification link.',
1746
+ status: 'code_sent',
1747
+ message: `Verification code sent to ${email}`,
1748
+ next_step: 'Ask the user to check their email for a 6-digit code, then call verify_code with the email and code.',
1663
1749
  email,
1664
- expires_in_minutes: 15,
1665
- next_step: 'Check your email, click the verification link, then run check_workspace_status',
1750
+ expires_in_minutes: 10,
1666
1751
  }, null, 2)
1667
1752
  }]
1668
1753
  };
@@ -1680,6 +1765,129 @@ Docs: https://apiclaw.cloud
1680
1765
  };
1681
1766
  }
1682
1767
  }
1768
+ case 'verify_code': {
1769
+ const email = args?.email || pendingRegistrationEmail;
1770
+ const code = args?.code;
1771
+ if (!email || !code) {
1772
+ return {
1773
+ content: [{
1774
+ type: 'text',
1775
+ text: JSON.stringify({
1776
+ status: 'error',
1777
+ error: 'Both email and code are required.',
1778
+ hint: 'Call register_owner first to receive a verification code.',
1779
+ }, null, 2)
1780
+ }],
1781
+ isError: true
1782
+ };
1783
+ }
1784
+ try {
1785
+ const fingerprint = getMachineFingerprint();
1786
+ const result = await convex.mutation("workspaces:verifyOTP", {
1787
+ email,
1788
+ code: code.trim(),
1789
+ fingerprint,
1790
+ });
1791
+ if (!result.success) {
1792
+ // Increment attempt counter
1793
+ try {
1794
+ await convex.mutation("workspaces:incrementOTPAttempt", { email, code: code.trim() });
1795
+ }
1796
+ catch (_) { }
1797
+ const reason = result.error === 'code_expired' ? 'expired'
1798
+ : result.error === 'attempts_exceeded' ? 'attempts_exceeded'
1799
+ : 'invalid';
1800
+ emitFunnelEvent({
1801
+ event: 'verify_code_failed',
1802
+ email,
1803
+ fingerprint: getMachineFingerprint(),
1804
+ mcpClient: detectMCPClient(),
1805
+ version: process.env.npm_package_version || 'unknown',
1806
+ props: { reason },
1807
+ });
1808
+ return {
1809
+ content: [{
1810
+ type: 'text',
1811
+ text: JSON.stringify({
1812
+ status: 'error',
1813
+ error: result.message || 'Verification failed',
1814
+ hint: result.error === 'code_expired'
1815
+ ? 'Run register_owner again to get a new code.'
1816
+ : 'Check the code and try again.',
1817
+ }, null, 2)
1818
+ }],
1819
+ isError: true
1820
+ };
1821
+ }
1822
+ // Success! Save session
1823
+ writeSession(result.sessionToken, result.workspace.id, result.workspace.email);
1824
+ // Claim anonymous usage
1825
+ try {
1826
+ const claimResult = await convex.mutation("workspaces:claimAnonymousUsage", {
1827
+ workspaceId: result.workspace.id,
1828
+ machineFingerprint: fingerprint,
1829
+ });
1830
+ if (claimResult.success && claimResult.claimedCount) {
1831
+ console.error(`[APIClaw] Claimed ${claimResult.claimedCount} anonymous usage records`);
1832
+ }
1833
+ }
1834
+ catch (_) { }
1835
+ // Update global context
1836
+ workspaceContext = {
1837
+ sessionToken: result.sessionToken,
1838
+ workspaceId: result.workspace.id,
1839
+ email: result.workspace.email,
1840
+ tier: result.workspace.tier,
1841
+ usageRemaining: result.workspace.usageLimit - result.workspace.usageCount,
1842
+ usageCount: result.workspace.usageCount,
1843
+ status: result.workspace.status,
1844
+ };
1845
+ pendingRegistrationEmail = null;
1846
+ // Funnel: verify_code (dedupe per workspace so re-verifies don't double-count)
1847
+ emitFunnelEvent({
1848
+ event: 'verify_code',
1849
+ email: result.workspace.email,
1850
+ workspaceId: result.workspace.id,
1851
+ fingerprint: getMachineFingerprint(),
1852
+ sessionToken: result.sessionToken,
1853
+ mcpClient: detectMCPClient(),
1854
+ platform: process.platform,
1855
+ version: process.env.npm_package_version || 'unknown',
1856
+ dedupeKey: `verify_code:${result.workspace.id}`,
1857
+ props: { isNewUser: !!result.isNewUser },
1858
+ });
1859
+ return {
1860
+ content: [{
1861
+ type: 'text',
1862
+ text: JSON.stringify({
1863
+ status: 'success',
1864
+ message: result.isNewUser
1865
+ ? `Welcome to APIClaw! Workspace activated for ${result.workspace.email}`
1866
+ : `Welcome back! Authenticated as ${result.workspace.email}`,
1867
+ workspace: {
1868
+ email: result.workspace.email,
1869
+ tier: result.workspace.tier,
1870
+ usageCount: result.workspace.usageCount,
1871
+ usageLimit: result.workspace.usageLimit,
1872
+ },
1873
+ ready: 'You can now use discover_apis and call_api.',
1874
+ }, null, 2)
1875
+ }]
1876
+ };
1877
+ }
1878
+ catch (error) {
1879
+ return {
1880
+ content: [{
1881
+ type: 'text',
1882
+ text: JSON.stringify({
1883
+ status: 'error',
1884
+ error: error instanceof Error ? error.message : 'Verification failed',
1885
+ }, null, 2)
1886
+ }],
1887
+ isError: true
1888
+ };
1889
+ }
1890
+ }
1683
1891
  case 'check_workspace_status': {
1684
1892
  // Check if we have a local session
1685
1893
  const session = readSession();
@@ -2016,21 +2224,10 @@ Docs: https://apiclaw.cloud
2016
2224
  isError: true
2017
2225
  };
2018
2226
  }
2019
- // Check workspace access
2020
- const access = checkWorkspaceAccess();
2021
- if (!access.allowed) {
2022
- return {
2023
- content: [{
2024
- type: 'text',
2025
- text: JSON.stringify({
2026
- status: 'error',
2027
- error: access.error,
2028
- hint: 'Use register_owner to authenticate your workspace.',
2029
- }, null, 2)
2030
- }],
2031
- isError: true
2032
- };
2033
- }
2227
+ // Registration gate: requireVerifiedOwner (single source of truth)
2228
+ const resumeGate = enforceOwner("mcp:resume_chain");
2229
+ if (!resumeGate.ok)
2230
+ return resumeGate.response;
2034
2231
  try {
2035
2232
  // Note: The resume_chain function requires the original chain definition
2036
2233
  // In practice, you'd store this or require the caller to provide it
@@ -2141,6 +2338,27 @@ async function main() {
2141
2338
  const transport = new StdioServerTransport();
2142
2339
  await server.connect(transport);
2143
2340
  trackStartup();
2341
+ // Funnel: first_run (once per fingerprint, persisted across restarts)
2342
+ try {
2343
+ const fp = getMachineFingerprint();
2344
+ const mcpClient = detectMCPClient();
2345
+ const version = process.env.npm_package_version || 'unknown';
2346
+ const dedupeKey = `first_run:${fp}`;
2347
+ if (!hasLocalMarker(dedupeKey)) {
2348
+ emitFunnelEvent({
2349
+ event: 'first_run',
2350
+ fingerprint: fp,
2351
+ mcpClient,
2352
+ platform: process.platform,
2353
+ version,
2354
+ dedupeKey,
2355
+ });
2356
+ setLocalMarker(dedupeKey);
2357
+ }
2358
+ }
2359
+ catch {
2360
+ /* non-blocking */
2361
+ }
2144
2362
  // Validate session on startup
2145
2363
  const hasValidSession = await validateSession();
2146
2364
  // Register/update agent identity (fire-and-forget)