@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/src/index.ts
CHANGED
|
@@ -45,6 +45,8 @@ import {
|
|
|
45
45
|
} from './confirmation.js';
|
|
46
46
|
import { executeCapability, listCapabilities, hasCapability } from './capability-router.js';
|
|
47
47
|
import { readSession, writeSession, clearSession, getMachineFingerprint, detectMCPClient, SessionData } from './session.js';
|
|
48
|
+
import { requireVerifiedOwner, type WorkspaceContextLike } from './registration-guard.js';
|
|
49
|
+
import { emitFunnelEvent, hasLocalMarker, setLocalMarker } from './funnel-client.js';
|
|
48
50
|
import { ConvexHttpClient } from 'convex/browser';
|
|
49
51
|
import {
|
|
50
52
|
getOrCreateCustomer,
|
|
@@ -84,6 +86,7 @@ interface WorkspaceContext {
|
|
|
84
86
|
|
|
85
87
|
let workspaceContext: WorkspaceContext | null = null;
|
|
86
88
|
let currentAgentId: string | null = null; // Agent ID from agents table (set on startup)
|
|
89
|
+
let pendingRegistrationEmail: string | null = null; // Email waiting for OTP verification
|
|
87
90
|
|
|
88
91
|
// Anonymous rate limit tracking (in-memory, per machine fingerprint)
|
|
89
92
|
interface AnonymousRateLimitState {
|
|
@@ -278,35 +281,21 @@ const rateLimitStore = new Map<string, RateLimitState>();
|
|
|
278
281
|
const UNREGISTERED_CALL_LIMIT = 5;
|
|
279
282
|
|
|
280
283
|
/**
|
|
281
|
-
*
|
|
284
|
+
* Check workspace access -- registration required for all API calls
|
|
282
285
|
*/
|
|
283
286
|
function checkWorkspaceAccess(providerId?: string): { allowed: boolean; error?: string; isAnonymous?: boolean } {
|
|
284
|
-
//
|
|
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
|
|
287
|
+
// All API calls require registration now
|
|
306
288
|
if (!workspaceContext) {
|
|
307
|
-
return {
|
|
308
|
-
allowed: false,
|
|
309
|
-
error:
|
|
289
|
+
return {
|
|
290
|
+
allowed: false,
|
|
291
|
+
error: JSON.stringify({
|
|
292
|
+
status: 'registration_required',
|
|
293
|
+
error: 'Registration required to call APIs.',
|
|
294
|
+
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.',
|
|
295
|
+
action: 'register_owner',
|
|
296
|
+
free_tier: '50 API calls/month -- completely free.',
|
|
297
|
+
}, null, 2),
|
|
298
|
+
isAnonymous: true,
|
|
310
299
|
};
|
|
311
300
|
}
|
|
312
301
|
|
|
@@ -355,6 +344,52 @@ function checkWorkspaceAccess(providerId?: string): { allowed: boolean; error?:
|
|
|
355
344
|
return { allowed: true, isAnonymous: false };
|
|
356
345
|
}
|
|
357
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Single enforcement entry point for every paying call path.
|
|
349
|
+
* Returns either a verified workspace context or an MCP-formatted block response.
|
|
350
|
+
*/
|
|
351
|
+
function enforceOwner(channel: string):
|
|
352
|
+
| { ok: true; ctx: WorkspaceContextLike }
|
|
353
|
+
| { ok: false; response: { content: { type: 'text'; text: string }[]; isError: true } } {
|
|
354
|
+
const result = requireVerifiedOwner(workspaceContext as WorkspaceContextLike | null);
|
|
355
|
+
if (result.ok) {
|
|
356
|
+
return { ok: true, ctx: result.ctx };
|
|
357
|
+
}
|
|
358
|
+
// Diagnostic: record why the call was blocked.
|
|
359
|
+
try {
|
|
360
|
+
emitFunnelEvent({
|
|
361
|
+
event: 'call_api_blocked',
|
|
362
|
+
workspaceId: workspaceContext?.workspaceId,
|
|
363
|
+
email: workspaceContext?.email,
|
|
364
|
+
fingerprint: getMachineFingerprint(),
|
|
365
|
+
mcpClient: detectMCPClient(),
|
|
366
|
+
platform: process.platform,
|
|
367
|
+
version: process.env.npm_package_version || 'unknown',
|
|
368
|
+
props: { reason: result.reason, channel },
|
|
369
|
+
});
|
|
370
|
+
if (result.reason === 'quota_exceeded') {
|
|
371
|
+
emitFunnelEvent({
|
|
372
|
+
event: 'quota_hit',
|
|
373
|
+
workspaceId: workspaceContext?.workspaceId,
|
|
374
|
+
email: workspaceContext?.email,
|
|
375
|
+
fingerprint: getMachineFingerprint(),
|
|
376
|
+
version: process.env.npm_package_version || 'unknown',
|
|
377
|
+
props: { tier: workspaceContext?.tier, limit: workspaceContext?.usageCount },
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
} catch { /* non-blocking */ }
|
|
381
|
+
return {
|
|
382
|
+
ok: false,
|
|
383
|
+
response: {
|
|
384
|
+
content: [{ type: 'text', text: JSON.stringify(result.payload, null, 2) }],
|
|
385
|
+
isError: true,
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Per-process marker: ensure first_call_api_success fires once per server boot.
|
|
391
|
+
let firstCallEmitted = false;
|
|
392
|
+
|
|
358
393
|
/**
|
|
359
394
|
* Get customer API key from environment variable
|
|
360
395
|
* Convention: {PROVIDER}_API_KEY (e.g., COACCEPT_API_KEY, ELKS_API_KEY)
|
|
@@ -511,7 +546,7 @@ const tools: Tool[] = [
|
|
|
511
546
|
},
|
|
512
547
|
{
|
|
513
548
|
name: 'call_api',
|
|
514
|
-
description: `Execute an API call through APIClaw.
|
|
549
|
+
description: `Execute an API call through APIClaw. Requires registration (free). If not registered, call register_owner first.
|
|
515
550
|
|
|
516
551
|
SINGLE CALL: Provide provider + action + params
|
|
517
552
|
CHAIN: Provide chain array to execute multiple APIs in sequence/parallel with cross-step references.
|
|
@@ -683,7 +718,7 @@ Example chain:
|
|
|
683
718
|
// ============================================
|
|
684
719
|
{
|
|
685
720
|
name: 'register_owner',
|
|
686
|
-
description: 'Register your email to create a workspace.
|
|
721
|
+
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
722
|
inputSchema: {
|
|
688
723
|
type: 'object',
|
|
689
724
|
properties: {
|
|
@@ -695,6 +730,24 @@ Example chain:
|
|
|
695
730
|
required: ['email']
|
|
696
731
|
}
|
|
697
732
|
},
|
|
733
|
+
{
|
|
734
|
+
name: 'verify_code',
|
|
735
|
+
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.',
|
|
736
|
+
inputSchema: {
|
|
737
|
+
type: 'object',
|
|
738
|
+
properties: {
|
|
739
|
+
email: {
|
|
740
|
+
type: 'string',
|
|
741
|
+
description: 'The email address used in register_owner'
|
|
742
|
+
},
|
|
743
|
+
code: {
|
|
744
|
+
type: 'string',
|
|
745
|
+
description: 'The 6-digit verification code from the email'
|
|
746
|
+
}
|
|
747
|
+
},
|
|
748
|
+
required: ['email', 'code']
|
|
749
|
+
}
|
|
750
|
+
},
|
|
698
751
|
{
|
|
699
752
|
name: 'check_workspace_status',
|
|
700
753
|
description: 'Check your workspace status, tier, and usage remaining.',
|
|
@@ -831,40 +884,36 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
831
884
|
try {
|
|
832
885
|
switch (name) {
|
|
833
886
|
case 'apiclaw_help': {
|
|
887
|
+
const isAuthenticated = !!workspaceContext;
|
|
834
888
|
const helpText = `
|
|
835
|
-
🦞 APIClaw
|
|
889
|
+
🦞 APIClaw -- The API Layer for AI Agents
|
|
836
890
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
837
|
-
|
|
838
|
-
|
|
891
|
+
${!isAuthenticated ? `
|
|
892
|
+
GET STARTED (free):
|
|
893
|
+
1. register_owner({ email: "you@example.com" }) — sends 6-digit code
|
|
894
|
+
2. verify_code({ email: "you@example.com", code: "123456" }) — activates workspace
|
|
895
|
+
` : `
|
|
896
|
+
STATUS: Authenticated as ${workspaceContext!.email} (${workspaceContext!.tier} tier)
|
|
897
|
+
`}
|
|
898
|
+
DISCOVER APIs (free, no registration needed):
|
|
839
899
|
discover_apis({ query: "send SMS to Sweden" })
|
|
840
|
-
discover_apis({ query: "search the web", max_results: 10 })
|
|
841
900
|
discover_apis({ query: "text to speech", category: "ai" })
|
|
842
901
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
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 })
|
|
902
|
+
CALL APIs (requires free registration):
|
|
903
|
+
call_api({ provider: "brave_search", action: "search", params: { q: "AI agents" } })
|
|
904
|
+
call_api({ provider: "elevenlabs", action: "tts", params: { text: "Hello" } })
|
|
905
|
+
|
|
906
|
+
23 MANAGED PROVIDERS:
|
|
907
|
+
OpenAI, Anthropic, xAI/Grok, Groq, Mistral, OpenRouter, Together AI,
|
|
908
|
+
Replicate, ElevenLabs, Deepgram, AssemblyAI, Brave Search, Firecrawl,
|
|
909
|
+
Serper, Resend, 46elks, Twilio, E2B, Stability AI, Cohere, Voyage AI,
|
|
910
|
+
GitHub, APILayer (27 sub-APIs)
|
|
911
|
+
|
|
912
|
+
26,700+ DISCOVERABLE | 1,654 CALLABLE | Free tier: 50 calls/month
|
|
865
913
|
|
|
866
914
|
Docs: https://apiclaw.cloud
|
|
867
915
|
`;
|
|
916
|
+
|
|
868
917
|
return {
|
|
869
918
|
content: [{ type: 'text', text: helpText }]
|
|
870
919
|
};
|
|
@@ -1180,6 +1229,12 @@ Docs: https://apiclaw.cloud
|
|
|
1180
1229
|
}
|
|
1181
1230
|
|
|
1182
1231
|
case 'call_api': {
|
|
1232
|
+
// ============================================
|
|
1233
|
+
// REGISTRATION GATE: requireVerifiedOwner (single source of truth)
|
|
1234
|
+
// ============================================
|
|
1235
|
+
const gate = enforceOwner("mcp:call_api");
|
|
1236
|
+
if (!gate.ok) return gate.response;
|
|
1237
|
+
|
|
1183
1238
|
const provider = args?.provider as string;
|
|
1184
1239
|
const action = args?.action as string;
|
|
1185
1240
|
const params = (args?.params as Record<string, any>) || {};
|
|
@@ -1188,7 +1243,7 @@ Docs: https://apiclaw.cloud
|
|
|
1188
1243
|
const chain = args?.chain as ChainStepUnion[] | undefined;
|
|
1189
1244
|
const subagentId = args?.subagent_id as string | undefined;
|
|
1190
1245
|
const aiBackend = args?.ai_backend as string | undefined;
|
|
1191
|
-
|
|
1246
|
+
|
|
1192
1247
|
// Track AI backend if provided
|
|
1193
1248
|
if (aiBackend && workspaceContext?.sessionToken) {
|
|
1194
1249
|
fetch('https://adventurous-avocet-799.convex.cloud/api/mutation', {
|
|
@@ -1209,34 +1264,7 @@ Docs: https://apiclaw.cloud
|
|
|
1209
1264
|
// CHAIN EXECUTION MODE
|
|
1210
1265
|
// ============================================
|
|
1211
1266
|
if (chain && Array.isArray(chain) && chain.length > 0) {
|
|
1212
|
-
//
|
|
1213
|
-
const access = checkWorkspaceAccess();
|
|
1214
|
-
if (!access.allowed) {
|
|
1215
|
-
// If error is already formatted JSON (from rate limit checks), return as-is
|
|
1216
|
-
if (access.error?.startsWith('{')) {
|
|
1217
|
-
return {
|
|
1218
|
-
content: [{
|
|
1219
|
-
type: 'text',
|
|
1220
|
-
text: access.error
|
|
1221
|
-
}],
|
|
1222
|
-
isError: true
|
|
1223
|
-
};
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
// Otherwise, wrap in standard error format
|
|
1227
|
-
return {
|
|
1228
|
-
content: [{
|
|
1229
|
-
type: 'text',
|
|
1230
|
-
text: JSON.stringify({
|
|
1231
|
-
status: 'error',
|
|
1232
|
-
error: access.error,
|
|
1233
|
-
hint: 'Use register_owner to authenticate your workspace.',
|
|
1234
|
-
}, null, 2)
|
|
1235
|
-
}],
|
|
1236
|
-
isError: true
|
|
1237
|
-
};
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1267
|
+
// Gate already enforced at top of call_api via enforceOwner().
|
|
1240
1268
|
try {
|
|
1241
1269
|
// Construct ChainDefinition from the input
|
|
1242
1270
|
const chainDefinition: ChainDefinition = {
|
|
@@ -1607,6 +1635,39 @@ Docs: https://apiclaw.cloud
|
|
|
1607
1635
|
workspaceContext.usageCount = (workspaceContext.usageCount || 0) + 1;
|
|
1608
1636
|
}
|
|
1609
1637
|
|
|
1638
|
+
// Funnel: call_api_error (provider-level failure)
|
|
1639
|
+
if (!result.success && workspaceContext) {
|
|
1640
|
+
emitFunnelEvent({
|
|
1641
|
+
event: 'call_api_error',
|
|
1642
|
+
workspaceId: workspaceContext.workspaceId,
|
|
1643
|
+
email: workspaceContext.email,
|
|
1644
|
+
fingerprint: getMachineFingerprint(),
|
|
1645
|
+
version: process.env.npm_package_version || 'unknown',
|
|
1646
|
+
props: {
|
|
1647
|
+
provider: result.provider || provider,
|
|
1648
|
+
action: result.action || action,
|
|
1649
|
+
errorCode: (result.error || '').slice(0, 80) || 'unknown',
|
|
1650
|
+
},
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// Funnel: first_call_api_success (once per workspace, deduped server-side)
|
|
1655
|
+
if (result.success && workspaceContext && !isFreeAPI && !firstCallEmitted) {
|
|
1656
|
+
firstCallEmitted = true;
|
|
1657
|
+
emitFunnelEvent({
|
|
1658
|
+
event: 'first_call_api_success',
|
|
1659
|
+
email: workspaceContext.email,
|
|
1660
|
+
workspaceId: workspaceContext.workspaceId,
|
|
1661
|
+
sessionToken: workspaceContext.sessionToken,
|
|
1662
|
+
fingerprint: getMachineFingerprint(),
|
|
1663
|
+
mcpClient: detectMCPClient(),
|
|
1664
|
+
platform: process.platform,
|
|
1665
|
+
version: process.env.npm_package_version || 'unknown',
|
|
1666
|
+
dedupeKey: `first_call:${workspaceContext.workspaceId}`,
|
|
1667
|
+
props: { provider, action, channel: 'mcp:call_api' },
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1610
1671
|
// Build response with signup nudge for unregistered users
|
|
1611
1672
|
const responseData: Record<string, unknown> = {
|
|
1612
1673
|
status: result.success ? 'success' : 'error',
|
|
@@ -1663,13 +1724,17 @@ Docs: https://apiclaw.cloud
|
|
|
1663
1724
|
}
|
|
1664
1725
|
|
|
1665
1726
|
case 'capability': {
|
|
1727
|
+
// Registration gate: requireVerifiedOwner (single source of truth)
|
|
1728
|
+
const capGate = enforceOwner("mcp:capability");
|
|
1729
|
+
if (!capGate.ok) return capGate.response;
|
|
1730
|
+
|
|
1666
1731
|
const capabilityId = args?.capability as string;
|
|
1667
1732
|
const action = args?.action as string;
|
|
1668
1733
|
const params = (args?.params as Record<string, any>) || {};
|
|
1669
1734
|
const preferences = (args?.preferences as Record<string, any>) || {};
|
|
1670
1735
|
const subagentId = args?.subagent_id as string | undefined;
|
|
1671
1736
|
const aiBackend = args?.ai_backend as string | undefined;
|
|
1672
|
-
|
|
1737
|
+
|
|
1673
1738
|
// Track AI backend if provided
|
|
1674
1739
|
if (aiBackend && workspaceContext?.sessionToken) {
|
|
1675
1740
|
fetch('https://adventurous-avocet-799.convex.cloud/api/mutation', {
|
|
@@ -1755,8 +1820,16 @@ Docs: https://apiclaw.cloud
|
|
|
1755
1820
|
|
|
1756
1821
|
case 'register_owner': {
|
|
1757
1822
|
const email = args?.email as string;
|
|
1758
|
-
|
|
1823
|
+
|
|
1759
1824
|
if (!email || !email.includes('@')) {
|
|
1825
|
+
emitFunnelEvent({
|
|
1826
|
+
event: 'register_owner_failed',
|
|
1827
|
+
email,
|
|
1828
|
+
fingerprint: getMachineFingerprint(),
|
|
1829
|
+
mcpClient: detectMCPClient(),
|
|
1830
|
+
version: process.env.npm_package_version || 'unknown',
|
|
1831
|
+
props: { reason: 'invalid_email' },
|
|
1832
|
+
});
|
|
1760
1833
|
return {
|
|
1761
1834
|
content: [{
|
|
1762
1835
|
type: 'text',
|
|
@@ -1768,13 +1841,12 @@ Docs: https://apiclaw.cloud
|
|
|
1768
1841
|
isError: true
|
|
1769
1842
|
};
|
|
1770
1843
|
}
|
|
1771
|
-
|
|
1844
|
+
|
|
1772
1845
|
try {
|
|
1773
|
-
// Check if workspace already exists
|
|
1846
|
+
// Check if workspace already exists and is active -- auto-login
|
|
1774
1847
|
const existing = await convex.query("workspaces:getByEmail" as any, { email }) as { id: string; status: string; tier: string; usageCount: number; usageLimit: number } | null;
|
|
1775
1848
|
|
|
1776
1849
|
if (existing && existing.status === 'active') {
|
|
1777
|
-
// Workspace exists and is active - create session directly
|
|
1778
1850
|
const fingerprint = getMachineFingerprint();
|
|
1779
1851
|
const sessionResult = await convex.mutation("workspaces:createAgentSession" as any, {
|
|
1780
1852
|
workspaceId: existing.id,
|
|
@@ -1783,23 +1855,17 @@ Docs: https://apiclaw.cloud
|
|
|
1783
1855
|
|
|
1784
1856
|
if (sessionResult.success) {
|
|
1785
1857
|
writeSession(sessionResult.sessionToken!, existing.id, email);
|
|
1786
|
-
|
|
1787
|
-
// Claim anonymous usage history
|
|
1858
|
+
|
|
1788
1859
|
try {
|
|
1789
1860
|
const claimResult = await convex.mutation("workspaces:claimAnonymousUsage" as any, {
|
|
1790
1861
|
workspaceId: existing.id,
|
|
1791
1862
|
machineFingerprint: fingerprint,
|
|
1792
|
-
}) as { success: boolean; claimedCount?: number
|
|
1793
|
-
|
|
1863
|
+
}) as { success: boolean; claimedCount?: number };
|
|
1794
1864
|
if (claimResult.success && claimResult.claimedCount) {
|
|
1795
|
-
console.error(`[APIClaw]
|
|
1865
|
+
console.error(`[APIClaw] Claimed ${claimResult.claimedCount} anonymous usage records`);
|
|
1796
1866
|
}
|
|
1797
|
-
} catch (
|
|
1798
|
-
|
|
1799
|
-
console.error('[APIClaw] Warning: Failed to claim anonymous usage:', err);
|
|
1800
|
-
}
|
|
1801
|
-
|
|
1802
|
-
// Update global context
|
|
1867
|
+
} catch (_) {}
|
|
1868
|
+
|
|
1803
1869
|
workspaceContext = {
|
|
1804
1870
|
sessionToken: sessionResult.sessionToken!,
|
|
1805
1871
|
workspaceId: existing.id,
|
|
@@ -1809,7 +1875,7 @@ Docs: https://apiclaw.cloud
|
|
|
1809
1875
|
usageCount: existing.usageCount,
|
|
1810
1876
|
status: existing.status,
|
|
1811
1877
|
};
|
|
1812
|
-
|
|
1878
|
+
|
|
1813
1879
|
return {
|
|
1814
1880
|
content: [{
|
|
1815
1881
|
type: 'text',
|
|
@@ -1827,29 +1893,15 @@ Docs: https://apiclaw.cloud
|
|
|
1827
1893
|
};
|
|
1828
1894
|
}
|
|
1829
1895
|
}
|
|
1830
|
-
|
|
1831
|
-
//
|
|
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
|
|
1896
|
+
|
|
1897
|
+
// New user or pending workspace -- send OTP
|
|
1844
1898
|
const fingerprint = getMachineFingerprint();
|
|
1845
|
-
const
|
|
1899
|
+
const otpResult = await convex.mutation("workspaces:createOTP" as any, {
|
|
1846
1900
|
email,
|
|
1847
1901
|
fingerprint,
|
|
1848
|
-
}) as {
|
|
1849
|
-
|
|
1850
|
-
// Send
|
|
1851
|
-
const verifyUrl = `https://apiclaw.cloud/auth/verify?token=${magicLinkResult.token}`;
|
|
1852
|
-
|
|
1902
|
+
}) as { code: string; expiresAt: number };
|
|
1903
|
+
|
|
1904
|
+
// Send OTP email
|
|
1853
1905
|
const emailResponse = await fetch('https://api.resend.com/emails', {
|
|
1854
1906
|
method: 'POST',
|
|
1855
1907
|
headers: {
|
|
@@ -1859,25 +1911,60 @@ Docs: https://apiclaw.cloud
|
|
|
1859
1911
|
body: JSON.stringify({
|
|
1860
1912
|
from: 'APIClaw <noreply@apiclaw.cloud>',
|
|
1861
1913
|
to: email,
|
|
1862
|
-
subject:
|
|
1863
|
-
html:
|
|
1914
|
+
subject: `Your APIClaw verification code: ${otpResult.code}`,
|
|
1915
|
+
html: `
|
|
1916
|
+
<div style="font-family: Inter, sans-serif; max-width: 480px; margin: 0 auto; padding: 40px 24px;">
|
|
1917
|
+
<div style="text-align: center; margin-bottom: 32px;">
|
|
1918
|
+
<span style="font-size: 48px;">🦞</span>
|
|
1919
|
+
</div>
|
|
1920
|
+
<h1 style="font-size: 24px; font-weight: 700; color: #0A0A0A; text-align: center; margin-bottom: 8px;">Your verification code</h1>
|
|
1921
|
+
<p style="font-size: 16px; color: #525252; text-align: center; margin-bottom: 32px;">Paste this code in your terminal to activate APIClaw.</p>
|
|
1922
|
+
<div style="background: #F5F5F5; border: 1px solid #E5E5E5; border-radius: 12px; padding: 24px; text-align: center; margin-bottom: 24px;">
|
|
1923
|
+
<code style="font-size: 36px; font-weight: 700; letter-spacing: 0.3em; color: #EF4444; font-family: 'JetBrains Mono', monospace;">${otpResult.code}</code>
|
|
1924
|
+
</div>
|
|
1925
|
+
<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>
|
|
1926
|
+
<hr style="border: none; border-top: 1px solid #E5E5E5; margin: 32px 0 16px;" />
|
|
1927
|
+
<p style="font-size: 12px; color: #A3A3A3; text-align: center;">APIClaw -- The API Layer For AI Agents</p>
|
|
1928
|
+
</div>
|
|
1929
|
+
`
|
|
1864
1930
|
})
|
|
1865
1931
|
});
|
|
1866
|
-
|
|
1932
|
+
|
|
1867
1933
|
if (!emailResponse.ok) {
|
|
1868
1934
|
const errorData = await emailResponse.text();
|
|
1935
|
+
emitFunnelEvent({
|
|
1936
|
+
event: 'register_owner_failed',
|
|
1937
|
+
email,
|
|
1938
|
+
fingerprint: getMachineFingerprint(),
|
|
1939
|
+
mcpClient: detectMCPClient(),
|
|
1940
|
+
version: process.env.npm_package_version || 'unknown',
|
|
1941
|
+
props: { reason: 'email_send_failed' },
|
|
1942
|
+
});
|
|
1869
1943
|
throw new Error(`Failed to send verification email: ${errorData}`);
|
|
1870
1944
|
}
|
|
1871
|
-
|
|
1945
|
+
|
|
1946
|
+
// Store pending email for verify_code
|
|
1947
|
+
pendingRegistrationEmail = email;
|
|
1948
|
+
|
|
1949
|
+
// Funnel: register_owner
|
|
1950
|
+
emitFunnelEvent({
|
|
1951
|
+
event: 'register_owner',
|
|
1952
|
+
email,
|
|
1953
|
+
fingerprint: getMachineFingerprint(),
|
|
1954
|
+
mcpClient: detectMCPClient(),
|
|
1955
|
+
platform: process.platform,
|
|
1956
|
+
version: process.env.npm_package_version || 'unknown',
|
|
1957
|
+
});
|
|
1958
|
+
|
|
1872
1959
|
return {
|
|
1873
1960
|
content: [{
|
|
1874
1961
|
type: 'text',
|
|
1875
1962
|
text: JSON.stringify({
|
|
1876
|
-
status: '
|
|
1877
|
-
message:
|
|
1963
|
+
status: 'code_sent',
|
|
1964
|
+
message: `Verification code sent to ${email}`,
|
|
1965
|
+
next_step: 'Ask the user to check their email for a 6-digit code, then call verify_code with the email and code.',
|
|
1878
1966
|
email,
|
|
1879
|
-
expires_in_minutes:
|
|
1880
|
-
next_step: 'Check your email, click the verification link, then run check_workspace_status',
|
|
1967
|
+
expires_in_minutes: 10,
|
|
1881
1968
|
}, null, 2)
|
|
1882
1969
|
}]
|
|
1883
1970
|
};
|
|
@@ -1894,6 +1981,146 @@ Docs: https://apiclaw.cloud
|
|
|
1894
1981
|
};
|
|
1895
1982
|
}
|
|
1896
1983
|
}
|
|
1984
|
+
|
|
1985
|
+
case 'verify_code': {
|
|
1986
|
+
const email = (args?.email as string) || pendingRegistrationEmail;
|
|
1987
|
+
const code = args?.code as string;
|
|
1988
|
+
|
|
1989
|
+
if (!email || !code) {
|
|
1990
|
+
return {
|
|
1991
|
+
content: [{
|
|
1992
|
+
type: 'text',
|
|
1993
|
+
text: JSON.stringify({
|
|
1994
|
+
status: 'error',
|
|
1995
|
+
error: 'Both email and code are required.',
|
|
1996
|
+
hint: 'Call register_owner first to receive a verification code.',
|
|
1997
|
+
}, null, 2)
|
|
1998
|
+
}],
|
|
1999
|
+
isError: true
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
try {
|
|
2004
|
+
const fingerprint = getMachineFingerprint();
|
|
2005
|
+
const result = await convex.mutation("workspaces:verifyOTP" as any, {
|
|
2006
|
+
email,
|
|
2007
|
+
code: code.trim(),
|
|
2008
|
+
fingerprint,
|
|
2009
|
+
}) as {
|
|
2010
|
+
success: boolean;
|
|
2011
|
+
error?: string;
|
|
2012
|
+
message?: string;
|
|
2013
|
+
isNewUser?: boolean;
|
|
2014
|
+
sessionToken?: string;
|
|
2015
|
+
workspace?: { id: string; email: string; tier: string; status: string; usageCount: number; usageLimit: number }
|
|
2016
|
+
};
|
|
2017
|
+
|
|
2018
|
+
if (!result.success) {
|
|
2019
|
+
// Increment attempt counter
|
|
2020
|
+
try {
|
|
2021
|
+
await convex.mutation("workspaces:incrementOTPAttempt" as any, { email, code: code.trim() });
|
|
2022
|
+
} catch (_) {}
|
|
2023
|
+
|
|
2024
|
+
const reason =
|
|
2025
|
+
result.error === 'code_expired' ? 'expired'
|
|
2026
|
+
: result.error === 'attempts_exceeded' ? 'attempts_exceeded'
|
|
2027
|
+
: 'invalid';
|
|
2028
|
+
emitFunnelEvent({
|
|
2029
|
+
event: 'verify_code_failed',
|
|
2030
|
+
email,
|
|
2031
|
+
fingerprint: getMachineFingerprint(),
|
|
2032
|
+
mcpClient: detectMCPClient(),
|
|
2033
|
+
version: process.env.npm_package_version || 'unknown',
|
|
2034
|
+
props: { reason },
|
|
2035
|
+
});
|
|
2036
|
+
|
|
2037
|
+
return {
|
|
2038
|
+
content: [{
|
|
2039
|
+
type: 'text',
|
|
2040
|
+
text: JSON.stringify({
|
|
2041
|
+
status: 'error',
|
|
2042
|
+
error: result.message || 'Verification failed',
|
|
2043
|
+
hint: result.error === 'code_expired'
|
|
2044
|
+
? 'Run register_owner again to get a new code.'
|
|
2045
|
+
: 'Check the code and try again.',
|
|
2046
|
+
}, null, 2)
|
|
2047
|
+
}],
|
|
2048
|
+
isError: true
|
|
2049
|
+
};
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
// Success! Save session
|
|
2053
|
+
writeSession(result.sessionToken!, result.workspace!.id, result.workspace!.email);
|
|
2054
|
+
|
|
2055
|
+
// Claim anonymous usage
|
|
2056
|
+
try {
|
|
2057
|
+
const claimResult = await convex.mutation("workspaces:claimAnonymousUsage" as any, {
|
|
2058
|
+
workspaceId: result.workspace!.id,
|
|
2059
|
+
machineFingerprint: fingerprint,
|
|
2060
|
+
}) as { success: boolean; claimedCount?: number };
|
|
2061
|
+
if (claimResult.success && claimResult.claimedCount) {
|
|
2062
|
+
console.error(`[APIClaw] Claimed ${claimResult.claimedCount} anonymous usage records`);
|
|
2063
|
+
}
|
|
2064
|
+
} catch (_) {}
|
|
2065
|
+
|
|
2066
|
+
// Update global context
|
|
2067
|
+
workspaceContext = {
|
|
2068
|
+
sessionToken: result.sessionToken!,
|
|
2069
|
+
workspaceId: result.workspace!.id,
|
|
2070
|
+
email: result.workspace!.email,
|
|
2071
|
+
tier: result.workspace!.tier,
|
|
2072
|
+
usageRemaining: result.workspace!.usageLimit - result.workspace!.usageCount,
|
|
2073
|
+
usageCount: result.workspace!.usageCount,
|
|
2074
|
+
status: result.workspace!.status,
|
|
2075
|
+
};
|
|
2076
|
+
|
|
2077
|
+
pendingRegistrationEmail = null;
|
|
2078
|
+
|
|
2079
|
+
// Funnel: verify_code (dedupe per workspace so re-verifies don't double-count)
|
|
2080
|
+
emitFunnelEvent({
|
|
2081
|
+
event: 'verify_code',
|
|
2082
|
+
email: result.workspace!.email,
|
|
2083
|
+
workspaceId: result.workspace!.id,
|
|
2084
|
+
fingerprint: getMachineFingerprint(),
|
|
2085
|
+
sessionToken: result.sessionToken,
|
|
2086
|
+
mcpClient: detectMCPClient(),
|
|
2087
|
+
platform: process.platform,
|
|
2088
|
+
version: process.env.npm_package_version || 'unknown',
|
|
2089
|
+
dedupeKey: `verify_code:${result.workspace!.id}`,
|
|
2090
|
+
props: { isNewUser: !!result.isNewUser },
|
|
2091
|
+
});
|
|
2092
|
+
|
|
2093
|
+
return {
|
|
2094
|
+
content: [{
|
|
2095
|
+
type: 'text',
|
|
2096
|
+
text: JSON.stringify({
|
|
2097
|
+
status: 'success',
|
|
2098
|
+
message: result.isNewUser
|
|
2099
|
+
? `Welcome to APIClaw! Workspace activated for ${result.workspace!.email}`
|
|
2100
|
+
: `Welcome back! Authenticated as ${result.workspace!.email}`,
|
|
2101
|
+
workspace: {
|
|
2102
|
+
email: result.workspace!.email,
|
|
2103
|
+
tier: result.workspace!.tier,
|
|
2104
|
+
usageCount: result.workspace!.usageCount,
|
|
2105
|
+
usageLimit: result.workspace!.usageLimit,
|
|
2106
|
+
},
|
|
2107
|
+
ready: 'You can now use discover_apis and call_api.',
|
|
2108
|
+
}, null, 2)
|
|
2109
|
+
}]
|
|
2110
|
+
};
|
|
2111
|
+
} catch (error) {
|
|
2112
|
+
return {
|
|
2113
|
+
content: [{
|
|
2114
|
+
type: 'text',
|
|
2115
|
+
text: JSON.stringify({
|
|
2116
|
+
status: 'error',
|
|
2117
|
+
error: error instanceof Error ? error.message : 'Verification failed',
|
|
2118
|
+
}, null, 2)
|
|
2119
|
+
}],
|
|
2120
|
+
isError: true
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
1897
2124
|
|
|
1898
2125
|
case 'check_workspace_status': {
|
|
1899
2126
|
// Check if we have a local session
|
|
@@ -2273,22 +2500,10 @@ Docs: https://apiclaw.cloud
|
|
|
2273
2500
|
};
|
|
2274
2501
|
}
|
|
2275
2502
|
|
|
2276
|
-
//
|
|
2277
|
-
const
|
|
2278
|
-
if (!
|
|
2279
|
-
|
|
2280
|
-
content: [{
|
|
2281
|
-
type: 'text',
|
|
2282
|
-
text: JSON.stringify({
|
|
2283
|
-
status: 'error',
|
|
2284
|
-
error: access.error,
|
|
2285
|
-
hint: 'Use register_owner to authenticate your workspace.',
|
|
2286
|
-
}, null, 2)
|
|
2287
|
-
}],
|
|
2288
|
-
isError: true
|
|
2289
|
-
};
|
|
2290
|
-
}
|
|
2291
|
-
|
|
2503
|
+
// Registration gate: requireVerifiedOwner (single source of truth)
|
|
2504
|
+
const resumeGate = enforceOwner("mcp:resume_chain");
|
|
2505
|
+
if (!resumeGate.ok) return resumeGate.response;
|
|
2506
|
+
|
|
2292
2507
|
try {
|
|
2293
2508
|
// Note: The resume_chain function requires the original chain definition
|
|
2294
2509
|
// In practice, you'd store this or require the caller to provide it
|
|
@@ -2411,7 +2626,28 @@ async function main() {
|
|
|
2411
2626
|
const transport = new StdioServerTransport();
|
|
2412
2627
|
await server.connect(transport);
|
|
2413
2628
|
trackStartup();
|
|
2414
|
-
|
|
2629
|
+
|
|
2630
|
+
// Funnel: first_run (once per fingerprint, persisted across restarts)
|
|
2631
|
+
try {
|
|
2632
|
+
const fp = getMachineFingerprint();
|
|
2633
|
+
const mcpClient = detectMCPClient();
|
|
2634
|
+
const version = process.env.npm_package_version || 'unknown';
|
|
2635
|
+
const dedupeKey = `first_run:${fp}`;
|
|
2636
|
+
if (!hasLocalMarker(dedupeKey)) {
|
|
2637
|
+
emitFunnelEvent({
|
|
2638
|
+
event: 'first_run',
|
|
2639
|
+
fingerprint: fp,
|
|
2640
|
+
mcpClient,
|
|
2641
|
+
platform: process.platform,
|
|
2642
|
+
version,
|
|
2643
|
+
dedupeKey,
|
|
2644
|
+
});
|
|
2645
|
+
setLocalMarker(dedupeKey);
|
|
2646
|
+
}
|
|
2647
|
+
} catch {
|
|
2648
|
+
/* non-blocking */
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2415
2651
|
// Validate session on startup
|
|
2416
2652
|
const hasValidSession = await validateSession();
|
|
2417
2653
|
|