@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 +13 -0
- package/convex/workspaces.ts +185 -0
- package/dist/index.js +210 -79
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +245 -100
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(),
|
package/convex/workspaces.ts
CHANGED
|
@@ -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
|
-
*
|
|
191
|
+
* Check workspace access -- registration required for all API calls
|
|
192
192
|
*/
|
|
193
193
|
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
|
|
194
|
+
// All API calls require registration now
|
|
213
195
|
if (!workspaceContext) {
|
|
214
196
|
return {
|
|
215
197
|
allowed: false,
|
|
216
|
-
error:
|
|
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.
|
|
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.
|
|
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
|
|
733
|
+
🦞 APIClaw -- The API Layer for AI Agents
|
|
726
734
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
727
|
-
|
|
728
|
-
|
|
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
|
-
|
|
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" } })
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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]
|
|
1619
|
+
console.error(`[APIClaw] Claimed ${claimResult.claimedCount} anonymous usage records`);
|
|
1587
1620
|
}
|
|
1588
1621
|
}
|
|
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
|
|
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
|
-
//
|
|
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
|
|
1651
|
+
const otpResult = await convex.mutation("workspaces:createOTP", {
|
|
1635
1652
|
email,
|
|
1636
1653
|
fingerprint,
|
|
1637
1654
|
});
|
|
1638
|
-
// Send
|
|
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:
|
|
1650
|
-
html:
|
|
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: '
|
|
1662
|
-
message:
|
|
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:
|
|
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();
|