@nordsym/apiclaw 1.3.5 → 1.3.7
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/README.md +33 -0
- package/convex/_generated/api.d.ts +12 -0
- package/convex/billing.ts +651 -216
- package/convex/crons.ts +17 -0
- package/convex/email.ts +135 -82
- package/convex/feedback.ts +265 -0
- package/convex/http.ts +80 -4
- package/convex/logs.ts +287 -0
- package/convex/providerKeys.ts +209 -0
- package/convex/providers.ts +18 -0
- package/convex/schema.ts +115 -0
- package/convex/stripeActions.ts +512 -0
- package/convex/webhooks.ts +494 -0
- package/convex/workspaces.ts +74 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +38 -7
- package/dist/cli.js.map +1 -1
- package/dist/index.js +178 -0
- package/dist/index.js.map +1 -1
- package/dist/metered.d.ts +62 -0
- package/dist/metered.d.ts.map +1 -0
- package/dist/metered.js +81 -0
- package/dist/metered.js.map +1 -0
- package/dist/stripe.d.ts +62 -0
- package/dist/stripe.d.ts.map +1 -1
- package/dist/stripe.js +212 -0
- package/dist/stripe.js.map +1 -1
- package/docs/PRD-final-polish.md +117 -0
- package/docs/PRD-mobile-responsive.md +56 -0
- package/docs/PRD-navigation-expansion.md +295 -0
- package/docs/PRD-stripe-billing.md +312 -0
- package/docs/PRD-workspace-cleanup.md +200 -0
- package/landing/src/app/api/billing/checkout/route.ts +109 -0
- package/landing/src/app/api/billing/payment-method/route.ts +118 -0
- package/landing/src/app/api/billing/portal/route.ts +64 -0
- package/landing/src/app/auth/verify/page.tsx +20 -5
- package/landing/src/app/earn/page.tsx +6 -6
- package/landing/src/app/login/page.tsx +1 -1
- package/landing/src/app/page.tsx +70 -70
- package/landing/src/app/providers/dashboard/page.tsx +1 -1
- package/landing/src/app/workspace/page.tsx +3497 -535
- package/landing/src/components/CheckoutButton.tsx +188 -0
- package/landing/src/components/Toast.tsx +84 -0
- package/landing/src/lib/stats.json +1 -1
- package/landing/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/cli.ts +57 -7
- package/src/index.ts +205 -0
- package/src/metered.ts +149 -0
- package/src/stripe.ts +253 -0
package/convex/logs.ts
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation, query } from "./_generated/server";
|
|
3
|
+
|
|
4
|
+
// ============================================
|
|
5
|
+
// MUTATIONS
|
|
6
|
+
// ============================================
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a log entry for an API call
|
|
10
|
+
* Called after each Direct Call execution
|
|
11
|
+
*/
|
|
12
|
+
export const createLog = mutation({
|
|
13
|
+
args: {
|
|
14
|
+
token: v.string(),
|
|
15
|
+
provider: v.string(),
|
|
16
|
+
action: v.string(),
|
|
17
|
+
status: v.union(v.literal("success"), v.literal("error")),
|
|
18
|
+
latencyMs: v.number(),
|
|
19
|
+
errorMessage: v.optional(v.string()),
|
|
20
|
+
},
|
|
21
|
+
handler: async (ctx, args) => {
|
|
22
|
+
// Verify session and get workspace
|
|
23
|
+
const session = await ctx.db
|
|
24
|
+
.query("agentSessions")
|
|
25
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.token))
|
|
26
|
+
.first();
|
|
27
|
+
|
|
28
|
+
if (!session) {
|
|
29
|
+
throw new Error("Invalid session token");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Create log entry
|
|
33
|
+
return await ctx.db.insert("apiLogs", {
|
|
34
|
+
workspaceId: session.workspaceId,
|
|
35
|
+
sessionToken: args.token,
|
|
36
|
+
provider: args.provider,
|
|
37
|
+
action: args.action,
|
|
38
|
+
status: args.status,
|
|
39
|
+
latencyMs: args.latencyMs,
|
|
40
|
+
errorMessage: args.errorMessage,
|
|
41
|
+
createdAt: Date.now(),
|
|
42
|
+
});
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Internal log creation (when workspaceId is already known)
|
|
48
|
+
* Used by execute functions that have already verified the session
|
|
49
|
+
*/
|
|
50
|
+
export const createLogInternal = mutation({
|
|
51
|
+
args: {
|
|
52
|
+
workspaceId: v.id("workspaces"),
|
|
53
|
+
sessionToken: v.string(),
|
|
54
|
+
provider: v.string(),
|
|
55
|
+
action: v.string(),
|
|
56
|
+
status: v.union(v.literal("success"), v.literal("error")),
|
|
57
|
+
latencyMs: v.number(),
|
|
58
|
+
errorMessage: v.optional(v.string()),
|
|
59
|
+
},
|
|
60
|
+
handler: async (ctx, args) => {
|
|
61
|
+
return await ctx.db.insert("apiLogs", {
|
|
62
|
+
workspaceId: args.workspaceId,
|
|
63
|
+
sessionToken: args.sessionToken,
|
|
64
|
+
provider: args.provider,
|
|
65
|
+
action: args.action,
|
|
66
|
+
status: args.status,
|
|
67
|
+
latencyMs: args.latencyMs,
|
|
68
|
+
errorMessage: args.errorMessage,
|
|
69
|
+
createdAt: Date.now(),
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ============================================
|
|
75
|
+
// QUERIES
|
|
76
|
+
// ============================================
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get logs for a workspace with pagination and filters
|
|
80
|
+
*/
|
|
81
|
+
export const getLogs = query({
|
|
82
|
+
args: {
|
|
83
|
+
token: v.string(),
|
|
84
|
+
limit: v.optional(v.number()),
|
|
85
|
+
cursor: v.optional(v.number()), // createdAt timestamp for pagination
|
|
86
|
+
status: v.optional(v.union(v.literal("success"), v.literal("error"), v.literal("all"))),
|
|
87
|
+
provider: v.optional(v.string()),
|
|
88
|
+
},
|
|
89
|
+
handler: async (ctx, args) => {
|
|
90
|
+
const limit = args.limit ?? 50;
|
|
91
|
+
const status = args.status ?? "all";
|
|
92
|
+
const provider = args.provider;
|
|
93
|
+
const cursor = args.cursor;
|
|
94
|
+
|
|
95
|
+
// Verify session
|
|
96
|
+
const session = await ctx.db
|
|
97
|
+
.query("agentSessions")
|
|
98
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.token))
|
|
99
|
+
.first();
|
|
100
|
+
|
|
101
|
+
if (!session) {
|
|
102
|
+
return { logs: [], hasMore: false };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Get logs for workspace
|
|
106
|
+
let query = ctx.db
|
|
107
|
+
.query("apiLogs")
|
|
108
|
+
.withIndex("by_workspaceId_createdAt", (q) => q.eq("workspaceId", session.workspaceId))
|
|
109
|
+
.order("desc");
|
|
110
|
+
|
|
111
|
+
// Apply cursor (pagination)
|
|
112
|
+
if (cursor) {
|
|
113
|
+
query = query.filter((q) => q.lt(q.field("createdAt"), cursor));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Collect more than limit to check hasMore
|
|
117
|
+
const allLogs = await query.take(limit + 1);
|
|
118
|
+
|
|
119
|
+
// Apply filters in-memory (Convex doesn't support complex compound filters)
|
|
120
|
+
let filteredLogs = allLogs;
|
|
121
|
+
|
|
122
|
+
if (status !== "all") {
|
|
123
|
+
filteredLogs = filteredLogs.filter((log) => log.status === status);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (provider && provider !== "all") {
|
|
127
|
+
filteredLogs = filteredLogs.filter((log) => log.provider === provider);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const hasMore = filteredLogs.length > limit;
|
|
131
|
+
const logs = filteredLogs.slice(0, limit);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
logs: logs.map((log) => ({
|
|
135
|
+
id: log._id,
|
|
136
|
+
provider: log.provider,
|
|
137
|
+
action: log.action,
|
|
138
|
+
status: log.status,
|
|
139
|
+
latencyMs: log.latencyMs,
|
|
140
|
+
errorMessage: log.errorMessage,
|
|
141
|
+
createdAt: log.createdAt,
|
|
142
|
+
})),
|
|
143
|
+
hasMore,
|
|
144
|
+
nextCursor: logs.length > 0 ? logs[logs.length - 1].createdAt : undefined,
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get aggregated log stats for workspace
|
|
151
|
+
*/
|
|
152
|
+
export const getLogStats = query({
|
|
153
|
+
args: {
|
|
154
|
+
token: v.string(),
|
|
155
|
+
periodDays: v.optional(v.number()),
|
|
156
|
+
},
|
|
157
|
+
handler: async (ctx, args) => {
|
|
158
|
+
const periodDays = args.periodDays ?? 7;
|
|
159
|
+
|
|
160
|
+
// Verify session
|
|
161
|
+
const session = await ctx.db
|
|
162
|
+
.query("agentSessions")
|
|
163
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.token))
|
|
164
|
+
.first();
|
|
165
|
+
|
|
166
|
+
if (!session) {
|
|
167
|
+
return {
|
|
168
|
+
totalCalls: 0,
|
|
169
|
+
successCount: 0,
|
|
170
|
+
errorCount: 0,
|
|
171
|
+
successRate: 0,
|
|
172
|
+
avgLatency: 0,
|
|
173
|
+
byProvider: [],
|
|
174
|
+
byDay: [],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const now = Date.now();
|
|
179
|
+
const periodStart = now - periodDays * 24 * 60 * 60 * 1000;
|
|
180
|
+
|
|
181
|
+
// Get all logs for this workspace in the period
|
|
182
|
+
const logs = await ctx.db
|
|
183
|
+
.query("apiLogs")
|
|
184
|
+
.withIndex("by_workspaceId_createdAt", (q) => q.eq("workspaceId", session.workspaceId))
|
|
185
|
+
.filter((q) => q.gte(q.field("createdAt"), periodStart))
|
|
186
|
+
.collect();
|
|
187
|
+
|
|
188
|
+
const totalCalls = logs.length;
|
|
189
|
+
const successCount = logs.filter((l) => l.status === "success").length;
|
|
190
|
+
const errorCount = logs.filter((l) => l.status === "error").length;
|
|
191
|
+
const successRate = totalCalls > 0 ? (successCount / totalCalls) * 100 : 0;
|
|
192
|
+
const totalLatency = logs.reduce((sum, l) => sum + l.latencyMs, 0);
|
|
193
|
+
const avgLatency = totalCalls > 0 ? Math.round(totalLatency / totalCalls) : 0;
|
|
194
|
+
|
|
195
|
+
// Group by provider
|
|
196
|
+
const byProviderMap: Record<string, { calls: number; success: number; error: number; latency: number }> = {};
|
|
197
|
+
for (const log of logs) {
|
|
198
|
+
if (!byProviderMap[log.provider]) {
|
|
199
|
+
byProviderMap[log.provider] = { calls: 0, success: 0, error: 0, latency: 0 };
|
|
200
|
+
}
|
|
201
|
+
byProviderMap[log.provider].calls++;
|
|
202
|
+
byProviderMap[log.provider].latency += log.latencyMs;
|
|
203
|
+
if (log.status === "success") {
|
|
204
|
+
byProviderMap[log.provider].success++;
|
|
205
|
+
} else {
|
|
206
|
+
byProviderMap[log.provider].error++;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const byProvider = Object.entries(byProviderMap)
|
|
211
|
+
.map(([provider, data]) => ({
|
|
212
|
+
provider,
|
|
213
|
+
calls: data.calls,
|
|
214
|
+
successRate: data.calls > 0 ? (data.success / data.calls) * 100 : 0,
|
|
215
|
+
avgLatency: data.calls > 0 ? Math.round(data.latency / data.calls) : 0,
|
|
216
|
+
}))
|
|
217
|
+
.sort((a, b) => b.calls - a.calls);
|
|
218
|
+
|
|
219
|
+
// Group by day
|
|
220
|
+
const byDayMap: Record<string, { calls: number; success: number; error: number }> = {};
|
|
221
|
+
for (const log of logs) {
|
|
222
|
+
const day = new Date(log.createdAt).toISOString().split("T")[0];
|
|
223
|
+
if (!byDayMap[day]) {
|
|
224
|
+
byDayMap[day] = { calls: 0, success: 0, error: 0 };
|
|
225
|
+
}
|
|
226
|
+
byDayMap[day].calls++;
|
|
227
|
+
if (log.status === "success") {
|
|
228
|
+
byDayMap[day].success++;
|
|
229
|
+
} else {
|
|
230
|
+
byDayMap[day].error++;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const byDay = Object.entries(byDayMap)
|
|
235
|
+
.map(([date, data]) => ({
|
|
236
|
+
date,
|
|
237
|
+
calls: data.calls,
|
|
238
|
+
success: data.success,
|
|
239
|
+
error: data.error,
|
|
240
|
+
}))
|
|
241
|
+
.sort((a, b) => a.date.localeCompare(b.date));
|
|
242
|
+
|
|
243
|
+
// Get unique providers for filter dropdown
|
|
244
|
+
const providers = [...new Set(logs.map((l) => l.provider))].sort();
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
totalCalls,
|
|
248
|
+
successCount,
|
|
249
|
+
errorCount,
|
|
250
|
+
successRate: Math.round(successRate * 10) / 10,
|
|
251
|
+
avgLatency,
|
|
252
|
+
byProvider,
|
|
253
|
+
byDay,
|
|
254
|
+
providers,
|
|
255
|
+
};
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Get unique providers for filter dropdown
|
|
261
|
+
*/
|
|
262
|
+
export const getProviders = query({
|
|
263
|
+
args: {
|
|
264
|
+
token: v.string(),
|
|
265
|
+
},
|
|
266
|
+
handler: async (ctx, args) => {
|
|
267
|
+
// Verify session
|
|
268
|
+
const session = await ctx.db
|
|
269
|
+
.query("agentSessions")
|
|
270
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.token))
|
|
271
|
+
.first();
|
|
272
|
+
|
|
273
|
+
if (!session) {
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Get all logs for this workspace
|
|
278
|
+
const logs = await ctx.db
|
|
279
|
+
.query("apiLogs")
|
|
280
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", session.workspaceId))
|
|
281
|
+
.collect();
|
|
282
|
+
|
|
283
|
+
// Get unique providers
|
|
284
|
+
const providers = [...new Set(logs.map((l) => l.provider))].sort();
|
|
285
|
+
return providers;
|
|
286
|
+
},
|
|
287
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation, query, internalQuery } from "./_generated/server";
|
|
3
|
+
|
|
4
|
+
// ============================================
|
|
5
|
+
// BYOK - Bring Your Own Key
|
|
6
|
+
// ============================================
|
|
7
|
+
|
|
8
|
+
// Supported providers for BYOK
|
|
9
|
+
export const BYOK_PROVIDERS = [
|
|
10
|
+
{ id: "brave_search", name: "Brave Search", icon: "🔍" },
|
|
11
|
+
{ id: "openrouter", name: "OpenRouter", icon: "🤖" },
|
|
12
|
+
{ id: "elevenlabs", name: "ElevenLabs", icon: "🎙️" },
|
|
13
|
+
{ id: "twilio", name: "Twilio", icon: "📞" },
|
|
14
|
+
{ id: "resend", name: "Resend", icon: "📧" },
|
|
15
|
+
{ id: "e2b", name: "E2B", icon: "💻" },
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
// Simple base64 encoding for MVP (proper encryption in production)
|
|
19
|
+
function encryptKey(key: string): string {
|
|
20
|
+
return Buffer.from(key).toString("base64");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function decryptKey(encryptedKey: string): string {
|
|
24
|
+
return Buffer.from(encryptedKey, "base64").toString("utf-8");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getKeyHint(key: string): string {
|
|
28
|
+
if (key.length <= 4) return "••••";
|
|
29
|
+
return key.slice(-4);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ============================================
|
|
33
|
+
// ADD KEY
|
|
34
|
+
// ============================================
|
|
35
|
+
|
|
36
|
+
export const addKey = mutation({
|
|
37
|
+
args: {
|
|
38
|
+
token: v.string(),
|
|
39
|
+
provider: v.string(),
|
|
40
|
+
apiKey: v.string(),
|
|
41
|
+
},
|
|
42
|
+
handler: async (ctx, args) => {
|
|
43
|
+
// Validate session
|
|
44
|
+
const session = await ctx.db
|
|
45
|
+
.query("agentSessions")
|
|
46
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.token))
|
|
47
|
+
.first();
|
|
48
|
+
|
|
49
|
+
if (!session) {
|
|
50
|
+
throw new Error("Invalid session");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const workspaceId = session.workspaceId;
|
|
54
|
+
|
|
55
|
+
// Check if key already exists for this provider
|
|
56
|
+
const existingKey = await ctx.db
|
|
57
|
+
.query("providerKeys")
|
|
58
|
+
.withIndex("by_provider", (q) =>
|
|
59
|
+
q.eq("workspaceId", workspaceId).eq("provider", args.provider)
|
|
60
|
+
)
|
|
61
|
+
.first();
|
|
62
|
+
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
const encryptedKey = encryptKey(args.apiKey);
|
|
65
|
+
const keyHint = getKeyHint(args.apiKey);
|
|
66
|
+
|
|
67
|
+
if (existingKey) {
|
|
68
|
+
// Update existing key
|
|
69
|
+
await ctx.db.patch(existingKey._id, {
|
|
70
|
+
encryptedKey,
|
|
71
|
+
keyHint,
|
|
72
|
+
updatedAt: now,
|
|
73
|
+
});
|
|
74
|
+
return { success: true, action: "updated" };
|
|
75
|
+
} else {
|
|
76
|
+
// Create new key
|
|
77
|
+
await ctx.db.insert("providerKeys", {
|
|
78
|
+
workspaceId,
|
|
79
|
+
provider: args.provider,
|
|
80
|
+
encryptedKey,
|
|
81
|
+
keyHint,
|
|
82
|
+
isCustom: false,
|
|
83
|
+
createdAt: now,
|
|
84
|
+
updatedAt: now,
|
|
85
|
+
});
|
|
86
|
+
return { success: true, action: "created" };
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// ============================================
|
|
92
|
+
// REMOVE KEY
|
|
93
|
+
// ============================================
|
|
94
|
+
|
|
95
|
+
export const removeKey = mutation({
|
|
96
|
+
args: {
|
|
97
|
+
token: v.string(),
|
|
98
|
+
provider: v.string(),
|
|
99
|
+
},
|
|
100
|
+
handler: async (ctx, args) => {
|
|
101
|
+
// Validate session
|
|
102
|
+
const session = await ctx.db
|
|
103
|
+
.query("agentSessions")
|
|
104
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.token))
|
|
105
|
+
.first();
|
|
106
|
+
|
|
107
|
+
if (!session) {
|
|
108
|
+
throw new Error("Invalid session");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const workspaceId = session.workspaceId;
|
|
112
|
+
|
|
113
|
+
// Find and delete the key
|
|
114
|
+
const existingKey = await ctx.db
|
|
115
|
+
.query("providerKeys")
|
|
116
|
+
.withIndex("by_provider", (q) =>
|
|
117
|
+
q.eq("workspaceId", workspaceId).eq("provider", args.provider)
|
|
118
|
+
)
|
|
119
|
+
.first();
|
|
120
|
+
|
|
121
|
+
if (!existingKey) {
|
|
122
|
+
throw new Error("Key not found");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await ctx.db.delete(existingKey._id);
|
|
126
|
+
return { success: true };
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ============================================
|
|
131
|
+
// GET KEYS (for display - no actual key values)
|
|
132
|
+
// ============================================
|
|
133
|
+
|
|
134
|
+
export const getKeys = query({
|
|
135
|
+
args: {
|
|
136
|
+
token: v.string(),
|
|
137
|
+
},
|
|
138
|
+
handler: async (ctx, args) => {
|
|
139
|
+
// Validate session
|
|
140
|
+
const session = await ctx.db
|
|
141
|
+
.query("agentSessions")
|
|
142
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.token))
|
|
143
|
+
.first();
|
|
144
|
+
|
|
145
|
+
if (!session) {
|
|
146
|
+
return { keys: [] };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const workspaceId = session.workspaceId;
|
|
150
|
+
|
|
151
|
+
// Get all keys for this workspace
|
|
152
|
+
const keys = await ctx.db
|
|
153
|
+
.query("providerKeys")
|
|
154
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", workspaceId))
|
|
155
|
+
.collect();
|
|
156
|
+
|
|
157
|
+
// Return without actual key values
|
|
158
|
+
return {
|
|
159
|
+
keys: keys.map((key) => ({
|
|
160
|
+
provider: key.provider,
|
|
161
|
+
keyHint: key.keyHint,
|
|
162
|
+
isCustom: key.isCustom,
|
|
163
|
+
customConfig: key.customConfig,
|
|
164
|
+
createdAt: key.createdAt,
|
|
165
|
+
updatedAt: key.updatedAt,
|
|
166
|
+
})),
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// ============================================
|
|
172
|
+
// GET KEY FOR EXECUTION (internal use only)
|
|
173
|
+
// ============================================
|
|
174
|
+
|
|
175
|
+
export const getKeyForExecution = internalQuery({
|
|
176
|
+
args: {
|
|
177
|
+
workspaceId: v.id("workspaces"),
|
|
178
|
+
provider: v.string(),
|
|
179
|
+
},
|
|
180
|
+
handler: async (ctx, args) => {
|
|
181
|
+
const key = await ctx.db
|
|
182
|
+
.query("providerKeys")
|
|
183
|
+
.withIndex("by_provider", (q) =>
|
|
184
|
+
q.eq("workspaceId", args.workspaceId).eq("provider", args.provider)
|
|
185
|
+
)
|
|
186
|
+
.first();
|
|
187
|
+
|
|
188
|
+
if (!key) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
apiKey: decryptKey(key.encryptedKey),
|
|
194
|
+
isCustom: key.isCustom,
|
|
195
|
+
customConfig: key.customConfig,
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// ============================================
|
|
201
|
+
// GET SUPPORTED PROVIDERS
|
|
202
|
+
// ============================================
|
|
203
|
+
|
|
204
|
+
export const getSupportedProviders = query({
|
|
205
|
+
args: {},
|
|
206
|
+
handler: async () => {
|
|
207
|
+
return BYOK_PROVIDERS;
|
|
208
|
+
},
|
|
209
|
+
});
|
package/convex/providers.ts
CHANGED
|
@@ -829,3 +829,21 @@ function generateToken(): string {
|
|
|
829
829
|
}
|
|
830
830
|
return result;
|
|
831
831
|
}
|
|
832
|
+
|
|
833
|
+
// Debug: Update API name/description
|
|
834
|
+
export const debugUpdateAPI = mutation({
|
|
835
|
+
args: {
|
|
836
|
+
apiId: v.string(),
|
|
837
|
+
name: v.optional(v.string()),
|
|
838
|
+
description: v.optional(v.string()),
|
|
839
|
+
category: v.optional(v.string()),
|
|
840
|
+
},
|
|
841
|
+
handler: async (ctx, args) => {
|
|
842
|
+
const updates: any = {};
|
|
843
|
+
if (args.name) updates.name = args.name;
|
|
844
|
+
if (args.description) updates.description = args.description;
|
|
845
|
+
if (args.category) updates.category = args.category;
|
|
846
|
+
await ctx.db.patch(args.apiId as any, updates);
|
|
847
|
+
return { updated: true };
|
|
848
|
+
},
|
|
849
|
+
});
|
package/convex/schema.ts
CHANGED
|
@@ -49,19 +49,57 @@ export default defineSchema({
|
|
|
49
49
|
tier: v.string(), // "free" | "pro" | "enterprise"
|
|
50
50
|
usageCount: v.number(), // total API calls made
|
|
51
51
|
usageLimit: v.number(), // max API calls for tier
|
|
52
|
+
// Stripe billing fields
|
|
52
53
|
stripeCustomerId: v.optional(v.string()),
|
|
54
|
+
stripeSubscriptionId: v.optional(v.string()),
|
|
55
|
+
billingPlan: v.optional(v.string()), // "free" | "usage_based" | "starter" | "pro" | "scale"
|
|
56
|
+
creditBalance: v.optional(v.number()), // prepaid credits in cents
|
|
57
|
+
lastBillingDate: v.optional(v.number()),
|
|
53
58
|
createdAt: v.number(),
|
|
54
59
|
updatedAt: v.number(),
|
|
55
60
|
})
|
|
56
61
|
.index("by_email", ["email"])
|
|
57
62
|
.index("by_stripeCustomerId", ["stripeCustomerId"])
|
|
63
|
+
.index("by_stripeSubscriptionId", ["stripeSubscriptionId"])
|
|
58
64
|
.index("by_status", ["status"]),
|
|
59
65
|
|
|
66
|
+
// Invoices (Stripe invoice records)
|
|
67
|
+
invoices: defineTable({
|
|
68
|
+
workspaceId: v.id("workspaces"),
|
|
69
|
+
stripeInvoiceId: v.string(),
|
|
70
|
+
amount: v.number(), // in cents
|
|
71
|
+
status: v.string(), // "paid" | "pending" | "failed" | "void"
|
|
72
|
+
periodStart: v.number(),
|
|
73
|
+
periodEnd: v.number(),
|
|
74
|
+
callCount: v.number(),
|
|
75
|
+
pdfUrl: v.optional(v.string()),
|
|
76
|
+
createdAt: v.number(),
|
|
77
|
+
})
|
|
78
|
+
.index("by_workspaceId", ["workspaceId"])
|
|
79
|
+
.index("by_stripeInvoiceId", ["stripeInvoiceId"])
|
|
80
|
+
.index("by_workspaceId_createdAt", ["workspaceId", "createdAt"]),
|
|
81
|
+
|
|
82
|
+
// Usage records (daily aggregation for billing)
|
|
83
|
+
usageRecords: defineTable({
|
|
84
|
+
workspaceId: v.id("workspaces"),
|
|
85
|
+
date: v.string(), // "2026-02-28" format
|
|
86
|
+
callCount: v.number(),
|
|
87
|
+
reportedToStripe: v.boolean(),
|
|
88
|
+
stripeUsageRecordId: v.optional(v.string()),
|
|
89
|
+
createdAt: v.number(),
|
|
90
|
+
updatedAt: v.number(),
|
|
91
|
+
})
|
|
92
|
+
.index("by_workspaceId", ["workspaceId"])
|
|
93
|
+
.index("by_date", ["date"])
|
|
94
|
+
.index("by_workspaceId_date", ["workspaceId", "date"])
|
|
95
|
+
.index("by_reportedToStripe", ["reportedToStripe"]),
|
|
96
|
+
|
|
60
97
|
// Agent sessions (for MCP server authentication)
|
|
61
98
|
agentSessions: defineTable({
|
|
62
99
|
workspaceId: v.id("workspaces"),
|
|
63
100
|
sessionToken: v.string(),
|
|
64
101
|
fingerprint: v.optional(v.string()), // machine fingerprint
|
|
102
|
+
customName: v.optional(v.string()), // user-defined name
|
|
65
103
|
lastUsedAt: v.number(),
|
|
66
104
|
createdAt: v.number(),
|
|
67
105
|
})
|
|
@@ -338,6 +376,24 @@ export default defineSchema({
|
|
|
338
376
|
.index("by_userId_providerId", ["userId", "providerId"])
|
|
339
377
|
.index("by_userId_timestamp", ["userId", "timestamp"]),
|
|
340
378
|
|
|
379
|
+
// ============================================
|
|
380
|
+
// API LOGS (workspace/consumer view)
|
|
381
|
+
// ============================================
|
|
382
|
+
|
|
383
|
+
apiLogs: defineTable({
|
|
384
|
+
workspaceId: v.id("workspaces"),
|
|
385
|
+
sessionToken: v.string(),
|
|
386
|
+
provider: v.string(),
|
|
387
|
+
action: v.string(),
|
|
388
|
+
status: v.union(v.literal("success"), v.literal("error")),
|
|
389
|
+
latencyMs: v.number(),
|
|
390
|
+
errorMessage: v.optional(v.string()),
|
|
391
|
+
createdAt: v.number(),
|
|
392
|
+
})
|
|
393
|
+
.index("by_workspaceId", ["workspaceId"])
|
|
394
|
+
.index("by_createdAt", ["createdAt"])
|
|
395
|
+
.index("by_workspaceId_createdAt", ["workspaceId", "createdAt"]),
|
|
396
|
+
|
|
341
397
|
// ============================================
|
|
342
398
|
// WAITLIST (for Direct Call provider leads)
|
|
343
399
|
// ============================================
|
|
@@ -413,4 +469,63 @@ export default defineSchema({
|
|
|
413
469
|
.index("by_providerId", ["providerId"])
|
|
414
470
|
.index("by_userId", ["userId"])
|
|
415
471
|
.index("by_timestamp", ["timestamp"]),
|
|
472
|
+
|
|
473
|
+
// ============================================
|
|
474
|
+
// WEBHOOKS
|
|
475
|
+
// ============================================
|
|
476
|
+
|
|
477
|
+
webhooks: defineTable({
|
|
478
|
+
workspaceId: v.id("workspaces"),
|
|
479
|
+
url: v.string(),
|
|
480
|
+
events: v.array(v.string()),
|
|
481
|
+
secret: v.string(), // For signature verification
|
|
482
|
+
enabled: v.boolean(),
|
|
483
|
+
lastTriggeredAt: v.optional(v.number()),
|
|
484
|
+
lastStatus: v.optional(v.string()), // "success" | "failed"
|
|
485
|
+
failCount: v.number(),
|
|
486
|
+
createdAt: v.number(),
|
|
487
|
+
})
|
|
488
|
+
.index("by_workspaceId", ["workspaceId"]),
|
|
489
|
+
|
|
490
|
+
// ============================================
|
|
491
|
+
// BYOK - BRING YOUR OWN KEY
|
|
492
|
+
// ============================================
|
|
493
|
+
|
|
494
|
+
// User-provided API keys for providers
|
|
495
|
+
providerKeys: defineTable({
|
|
496
|
+
workspaceId: v.id("workspaces"),
|
|
497
|
+
provider: v.string(), // "brave_search", "openrouter", etc.
|
|
498
|
+
encryptedKey: v.string(), // Base64 encoded for MVP
|
|
499
|
+
keyHint: v.string(), // Last 4 chars for display
|
|
500
|
+
isCustom: v.boolean(), // true if custom provider (not built-in)
|
|
501
|
+
customConfig: v.optional(v.object({
|
|
502
|
+
baseUrl: v.string(),
|
|
503
|
+
authType: v.string(), // "bearer", "api_key", "basic"
|
|
504
|
+
authHeader: v.optional(v.string()), // e.g. "X-API-Key"
|
|
505
|
+
})),
|
|
506
|
+
createdAt: v.number(),
|
|
507
|
+
updatedAt: v.number(),
|
|
508
|
+
})
|
|
509
|
+
.index("by_workspaceId", ["workspaceId"])
|
|
510
|
+
.index("by_provider", ["workspaceId", "provider"]),
|
|
511
|
+
|
|
512
|
+
// ============================================
|
|
513
|
+
// FEEDBACK SYSTEM
|
|
514
|
+
// ============================================
|
|
515
|
+
|
|
516
|
+
// User feedback with voting
|
|
517
|
+
feedback: defineTable({
|
|
518
|
+
workspaceId: v.id("workspaces"),
|
|
519
|
+
type: v.union(v.literal("bug"), v.literal("feature"), v.literal("general")),
|
|
520
|
+
content: v.string(),
|
|
521
|
+
votes: v.number(),
|
|
522
|
+
votedBy: v.array(v.string()), // workspace IDs that voted
|
|
523
|
+
status: v.union(v.literal("new"), v.literal("reviewing"), v.literal("planned"), v.literal("shipped")),
|
|
524
|
+
createdAt: v.number(),
|
|
525
|
+
})
|
|
526
|
+
.index("by_workspaceId", ["workspaceId"])
|
|
527
|
+
.index("by_type", ["type"])
|
|
528
|
+
.index("by_status", ["status"])
|
|
529
|
+
.index("by_votes", ["votes"])
|
|
530
|
+
.index("by_createdAt", ["createdAt"]),
|
|
416
531
|
});
|