@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/.claude/settings.local.json +5 -1
- package/convex/_generated/api.d.ts +8 -0
- package/convex/_listWorkspaces.ts +13 -0
- package/convex/crons.ts +15 -0
- package/convex/funnel.ts +431 -0
- package/convex/guards.ts +174 -0
- package/convex/http.ts +334 -8
- package/convex/nurture.ts +355 -0
- package/convex/schema.ts +70 -0
- package/convex/workspaces.ts +185 -0
- package/dist/funnel-client.d.ts +24 -0
- package/dist/funnel-client.d.ts.map +1 -0
- package/dist/funnel-client.js +131 -0
- package/dist/funnel-client.js.map +1 -0
- package/dist/funnel.test.d.ts +2 -0
- package/dist/funnel.test.d.ts.map +1 -0
- package/dist/funnel.test.js +145 -0
- package/dist/funnel.test.js.map +1 -0
- package/dist/index.js +338 -120
- package/dist/index.js.map +1 -1
- package/dist/postinstall.d.ts +0 -5
- package/dist/postinstall.d.ts.map +1 -1
- package/dist/postinstall.js +24 -3
- package/dist/postinstall.js.map +1 -1
- package/dist/registration-guard.d.ts +29 -0
- package/dist/registration-guard.d.ts.map +1 -0
- package/dist/registration-guard.js +87 -0
- package/dist/registration-guard.js.map +1 -0
- package/package.json +1 -1
- package/src/funnel-client.ts +168 -0
- package/src/funnel.test.ts +187 -0
- package/src/index.ts +381 -145
- package/src/postinstall.ts +24 -2
- package/src/registration-guard.ts +117 -0
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
|
-
*
|
|
193
|
+
* Check workspace access -- registration required for all API calls
|
|
192
194
|
*/
|
|
193
195
|
function checkWorkspaceAccess(providerId) {
|
|
194
|
-
//
|
|
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:
|
|
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.
|
|
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.
|
|
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
|
|
778
|
+
🦞 APIClaw -- The API Layer for AI Agents
|
|
726
779
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
727
|
-
|
|
728
|
-
|
|
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
|
-
|
|
734
|
-
|
|
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
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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]
|
|
1655
|
+
console.error(`[APIClaw] Claimed ${claimResult.claimedCount} anonymous usage records`);
|
|
1587
1656
|
}
|
|
1588
1657
|
}
|
|
1589
|
-
catch (
|
|
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
|
-
//
|
|
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
|
|
1687
|
+
const otpResult = await convex.mutation("workspaces:createOTP", {
|
|
1635
1688
|
email,
|
|
1636
1689
|
fingerprint,
|
|
1637
1690
|
});
|
|
1638
|
-
// Send
|
|
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:
|
|
1650
|
-
html:
|
|
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: '
|
|
1662
|
-
message:
|
|
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:
|
|
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
|
-
//
|
|
2020
|
-
const
|
|
2021
|
-
if (!
|
|
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)
|