@nordsym/apiclaw 1.7.2 → 1.7.4
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/_generated/api.d.ts +115 -0
- package/convex/_generated/api.js +23 -0
- package/convex/_generated/dataModel.d.ts +60 -0
- package/convex/_generated/server.d.ts +143 -0
- package/convex/_generated/server.js +93 -0
- package/convex/adminActivate.d.ts +3 -0
- package/convex/adminActivate.d.ts.map +1 -0
- package/convex/adminActivate.js +47 -0
- package/convex/adminActivate.js.map +1 -0
- package/convex/adminActivate.ts +54 -0
- package/convex/adminStats.d.ts +3 -0
- package/convex/adminStats.d.ts.map +1 -0
- package/convex/adminStats.js +42 -0
- package/convex/adminStats.js.map +1 -0
- package/convex/adminStats.ts +44 -0
- package/convex/agents.d.ts +76 -0
- package/convex/agents.d.ts.map +1 -0
- package/convex/agents.js +699 -0
- package/convex/agents.js.map +1 -0
- package/convex/agents.ts +814 -0
- package/convex/analytics.d.ts +5 -0
- package/convex/analytics.d.ts.map +1 -0
- package/convex/analytics.js +166 -0
- package/convex/analytics.js.map +1 -0
- package/convex/analytics.ts +186 -0
- package/convex/billing.d.ts +88 -0
- package/convex/billing.d.ts.map +1 -0
- package/convex/billing.js +655 -0
- package/convex/billing.js.map +1 -0
- package/convex/billing.ts +791 -0
- package/convex/capabilities.d.ts +9 -0
- package/convex/capabilities.d.ts.map +1 -0
- package/convex/capabilities.js +145 -0
- package/convex/capabilities.js.map +1 -0
- package/convex/capabilities.ts +157 -0
- package/convex/chains.d.ts +68 -0
- package/convex/chains.d.ts.map +1 -0
- package/convex/chains.js +1105 -0
- package/convex/chains.js.map +1 -0
- package/convex/chains.ts +1318 -0
- package/convex/credits.d.ts +25 -0
- package/convex/credits.d.ts.map +1 -0
- package/convex/credits.js +186 -0
- package/convex/credits.js.map +1 -0
- package/convex/credits.ts +211 -0
- package/convex/crons.d.ts +3 -0
- package/convex/crons.d.ts.map +1 -0
- package/convex/crons.js +17 -0
- package/convex/crons.js.map +1 -0
- package/convex/crons.ts +28 -0
- package/convex/directCall.d.ts +72 -0
- package/convex/directCall.d.ts.map +1 -0
- package/convex/directCall.js +627 -0
- package/convex/directCall.js.map +1 -0
- package/convex/directCall.ts +678 -0
- package/convex/earnProgress.d.ts +58 -0
- package/convex/earnProgress.d.ts.map +1 -0
- package/convex/earnProgress.js +649 -0
- package/convex/earnProgress.js.map +1 -0
- package/convex/earnProgress.ts +753 -0
- package/convex/email.d.ts +14 -0
- package/convex/email.d.ts.map +1 -0
- package/convex/email.js +300 -0
- package/convex/email.js.map +1 -0
- package/convex/email.ts +329 -0
- package/convex/feedback.d.ts +7 -0
- package/convex/feedback.d.ts.map +1 -0
- package/convex/feedback.js +227 -0
- package/convex/feedback.js.map +1 -0
- package/convex/feedback.ts +265 -0
- package/convex/http.d.ts +3 -0
- package/convex/http.d.ts.map +1 -0
- package/convex/http.js +1405 -0
- package/convex/http.js.map +1 -0
- package/convex/http.ts +1577 -0
- package/convex/inbound.d.ts +2 -0
- package/convex/inbound.d.ts.map +1 -0
- package/convex/inbound.js +32 -0
- package/convex/inbound.js.map +1 -0
- package/convex/inbound.ts +32 -0
- package/convex/logs.d.ts +38 -0
- package/convex/logs.d.ts.map +1 -0
- package/convex/logs.js +487 -0
- package/convex/logs.js.map +1 -0
- package/convex/logs.ts +550 -0
- package/convex/mou.d.ts +6 -0
- package/convex/mou.d.ts.map +1 -0
- package/convex/mou.js +82 -0
- package/convex/mou.js.map +1 -0
- package/convex/mou.ts +91 -0
- package/convex/providerKeys.d.ts +31 -0
- package/convex/providerKeys.d.ts.map +1 -0
- package/convex/providerKeys.js +257 -0
- package/convex/providerKeys.js.map +1 -0
- package/convex/providerKeys.ts +289 -0
- package/convex/providers.d.ts +32 -0
- package/convex/providers.d.ts.map +1 -0
- package/convex/providers.js +814 -0
- package/convex/providers.js.map +1 -0
- package/convex/providers.ts +909 -0
- package/convex/purchases.d.ts +7 -0
- package/convex/purchases.d.ts.map +1 -0
- package/convex/purchases.js +157 -0
- package/convex/purchases.js.map +1 -0
- package/convex/purchases.ts +183 -0
- package/convex/ratelimit.d.ts +4 -0
- package/convex/ratelimit.d.ts.map +1 -0
- package/convex/ratelimit.js +91 -0
- package/convex/ratelimit.js.map +1 -0
- package/convex/ratelimit.ts +104 -0
- package/convex/schema.ts +802 -0
- package/convex/searchLogs.d.ts +4 -0
- package/convex/searchLogs.d.ts.map +1 -0
- package/convex/searchLogs.js +129 -0
- package/convex/searchLogs.js.map +1 -0
- package/convex/searchLogs.ts +146 -0
- package/convex/seedAPILayerAPIs.d.ts +7 -0
- package/convex/seedAPILayerAPIs.d.ts.map +1 -0
- package/convex/seedAPILayerAPIs.js +177 -0
- package/convex/seedAPILayerAPIs.js.map +1 -0
- package/convex/seedAPILayerAPIs.ts +191 -0
- package/convex/seedDirectCallConfigs.d.ts +2 -0
- package/convex/seedDirectCallConfigs.d.ts.map +1 -0
- package/convex/seedDirectCallConfigs.js +324 -0
- package/convex/seedDirectCallConfigs.js.map +1 -0
- package/convex/seedDirectCallConfigs.ts +336 -0
- package/convex/seedPratham.d.ts +6 -0
- package/convex/seedPratham.d.ts.map +1 -0
- package/convex/seedPratham.js +150 -0
- package/convex/seedPratham.js.map +1 -0
- package/convex/seedPratham.ts +161 -0
- package/convex/spendAlerts.d.ts +36 -0
- package/convex/spendAlerts.d.ts.map +1 -0
- package/convex/spendAlerts.js +380 -0
- package/convex/spendAlerts.js.map +1 -0
- package/convex/spendAlerts.ts +442 -0
- package/convex/stripeActions.d.ts +19 -0
- package/convex/stripeActions.d.ts.map +1 -0
- package/convex/stripeActions.js +411 -0
- package/convex/stripeActions.js.map +1 -0
- package/convex/stripeActions.ts +512 -0
- package/convex/teams.d.ts +21 -0
- package/convex/teams.d.ts.map +1 -0
- package/convex/teams.js +215 -0
- package/convex/teams.js.map +1 -0
- package/convex/teams.ts +243 -0
- package/convex/telemetry.d.ts +4 -0
- package/convex/telemetry.d.ts.map +1 -0
- package/convex/telemetry.js +74 -0
- package/convex/telemetry.js.map +1 -0
- package/convex/telemetry.ts +81 -0
- package/convex/tsconfig.json +25 -0
- package/convex/updateAPIStatus.d.ts +6 -0
- package/convex/updateAPIStatus.d.ts.map +1 -0
- package/convex/updateAPIStatus.js +40 -0
- package/convex/updateAPIStatus.js.map +1 -0
- package/convex/updateAPIStatus.ts +45 -0
- package/convex/usage.d.ts +27 -0
- package/convex/usage.d.ts.map +1 -0
- package/convex/usage.js +229 -0
- package/convex/usage.js.map +1 -0
- package/convex/usage.ts +260 -0
- package/convex/waitlist.d.ts +4 -0
- package/convex/waitlist.d.ts.map +1 -0
- package/convex/waitlist.js +49 -0
- package/convex/waitlist.js.map +1 -0
- package/convex/waitlist.ts +55 -0
- package/convex/webhooks.d.ts +12 -0
- package/convex/webhooks.d.ts.map +1 -0
- package/convex/webhooks.js +410 -0
- package/convex/webhooks.js.map +1 -0
- package/convex/webhooks.ts +494 -0
- package/convex/workspaces.d.ts +31 -0
- package/convex/workspaces.d.ts.map +1 -0
- package/convex/workspaces.js +975 -0
- package/convex/workspaces.js.map +1 -0
- package/convex/workspaces.ts +1130 -0
- package/dist/bin.js +0 -0
- package/dist/capability-router.js +1 -1
- package/dist/capability-router.js.map +1 -1
- package/dist/execute.d.ts +2 -0
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +18 -4
- package/dist/execute.js.map +1 -1
- package/dist/http-api.js +1 -1
- package/dist/http-api.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-analytics.d.ts +32 -0
- package/dist/mcp-analytics.d.ts.map +1 -0
- package/dist/mcp-analytics.js +130 -0
- package/dist/mcp-analytics.js.map +1 -0
- package/package.json +1 -1
- package/dist/chain-types.d.ts +0 -187
- package/dist/chain-types.d.ts.map +0 -1
- package/dist/chain-types.js +0 -33
- package/dist/chain-types.js.map +0 -1
- package/dist/registry/apis.json.bak +0 -248811
- package/dist/src/bin.js +0 -17
- package/dist/src/capability-router.js +0 -240
- package/dist/src/chainExecutor.js +0 -451
- package/dist/src/chainResolver.js +0 -518
- package/dist/src/cli/commands/doctor.js +0 -324
- package/dist/src/cli/commands/mcp-install.js +0 -255
- package/dist/src/cli/commands/restore.js +0 -259
- package/dist/src/cli/commands/setup.js +0 -205
- package/dist/src/cli/commands/uninstall.js +0 -188
- package/dist/src/cli/index.js +0 -111
- package/dist/src/cli.js +0 -302
- package/dist/src/confirmation.js +0 -240
- package/dist/src/credentials.js +0 -357
- package/dist/src/credits.js +0 -260
- package/dist/src/crypto.js +0 -66
- package/dist/src/discovery.js +0 -504
- package/dist/src/enterprise/env.js +0 -123
- package/dist/src/enterprise/script-generator.js +0 -460
- package/dist/src/execute-dynamic.js +0 -473
- package/dist/src/execute.js +0 -1727
- package/dist/src/index.js +0 -2062
- package/dist/src/metered.js +0 -80
- package/dist/src/open-apis.js +0 -276
- package/dist/src/proxy.js +0 -28
- package/dist/src/session.js +0 -86
- package/dist/src/stripe.js +0 -407
- package/dist/src/telemetry.js +0 -49
- package/dist/src/types.js +0 -2
- package/dist/src/utils/backup.js +0 -181
- package/dist/src/utils/config.js +0 -220
- package/dist/src/utils/os.js +0 -105
- package/dist/src/utils/paths.js +0 -159
|
@@ -0,0 +1,975 @@
|
|
|
1
|
+
import { mutation, query } from "./_generated/server";
|
|
2
|
+
import { internal } from "./_generated/api";
|
|
3
|
+
import { v } from "convex/values";
|
|
4
|
+
// ============================================
|
|
5
|
+
// MAGIC LINK AUTH FOR WORKSPACES
|
|
6
|
+
// ============================================
|
|
7
|
+
// Create magic link for workspace email auth
|
|
8
|
+
export const createMagicLink = mutation({
|
|
9
|
+
args: {
|
|
10
|
+
email: v.string(),
|
|
11
|
+
fingerprint: v.optional(v.string()),
|
|
12
|
+
},
|
|
13
|
+
handler: async (ctx, { email, fingerprint }) => {
|
|
14
|
+
const token = generateToken();
|
|
15
|
+
const expiresAt = Date.now() + 15 * 60 * 1000; // 15 minutes
|
|
16
|
+
await ctx.db.insert("workspaceMagicLinks", {
|
|
17
|
+
email: email.toLowerCase(),
|
|
18
|
+
token,
|
|
19
|
+
sessionFingerprint: fingerprint,
|
|
20
|
+
expiresAt,
|
|
21
|
+
createdAt: Date.now(),
|
|
22
|
+
});
|
|
23
|
+
return { token, expiresAt };
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
// Generate a unique referral code (CLAW-XXXXXX format)
|
|
27
|
+
function generateReferralCode() {
|
|
28
|
+
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
29
|
+
let code = "";
|
|
30
|
+
for (let i = 0; i < 6; i++) {
|
|
31
|
+
code += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
32
|
+
}
|
|
33
|
+
return `CLAW-${code}`;
|
|
34
|
+
}
|
|
35
|
+
// Verify magic link and create workspace + session
|
|
36
|
+
export const verifyMagicLink = mutation({
|
|
37
|
+
args: {
|
|
38
|
+
token: v.string(),
|
|
39
|
+
fingerprint: v.optional(v.string()),
|
|
40
|
+
referralCode: v.optional(v.string()), // Referral code from signup URL
|
|
41
|
+
},
|
|
42
|
+
handler: async (ctx, { token, fingerprint, referralCode }) => {
|
|
43
|
+
const magicLink = await ctx.db
|
|
44
|
+
.query("workspaceMagicLinks")
|
|
45
|
+
.withIndex("by_token", (q) => q.eq("token", token))
|
|
46
|
+
.first();
|
|
47
|
+
if (!magicLink) {
|
|
48
|
+
return { success: false, error: "Invalid token" };
|
|
49
|
+
}
|
|
50
|
+
if (magicLink.expiresAt < Date.now()) {
|
|
51
|
+
return { success: false, error: "Token expired" };
|
|
52
|
+
}
|
|
53
|
+
if (magicLink.usedAt) {
|
|
54
|
+
return { success: false, error: "Token already used" };
|
|
55
|
+
}
|
|
56
|
+
// Mark as used
|
|
57
|
+
await ctx.db.patch(magicLink._id, { usedAt: Date.now() });
|
|
58
|
+
// Find or create workspace
|
|
59
|
+
let workspace = await ctx.db
|
|
60
|
+
.query("workspaces")
|
|
61
|
+
.withIndex("by_email", (q) => q.eq("email", magicLink.email))
|
|
62
|
+
.first();
|
|
63
|
+
let isNewUser = false;
|
|
64
|
+
if (!workspace) {
|
|
65
|
+
isNewUser = true;
|
|
66
|
+
// Generate unique referral code for new user
|
|
67
|
+
let newReferralCode;
|
|
68
|
+
let attempts = 0;
|
|
69
|
+
do {
|
|
70
|
+
newReferralCode = generateReferralCode();
|
|
71
|
+
const existing = await ctx.db
|
|
72
|
+
.query("workspaces")
|
|
73
|
+
.withIndex("by_referralCode", (q) => q.eq("referralCode", newReferralCode))
|
|
74
|
+
.first();
|
|
75
|
+
if (!existing)
|
|
76
|
+
break;
|
|
77
|
+
attempts++;
|
|
78
|
+
} while (attempts < 10);
|
|
79
|
+
// Create new workspace with free tier + referral code
|
|
80
|
+
const workspaceId = await ctx.db.insert("workspaces", {
|
|
81
|
+
email: magicLink.email,
|
|
82
|
+
status: "active",
|
|
83
|
+
tier: "free",
|
|
84
|
+
usageCount: 0,
|
|
85
|
+
usageLimit: 50, // Legacy field, now using weekly limits
|
|
86
|
+
weeklyUsageCount: 0,
|
|
87
|
+
weeklyUsageLimit: 50, // 50 calls/week for free tier
|
|
88
|
+
hourlyUsageCount: 0,
|
|
89
|
+
referralCode: newReferralCode,
|
|
90
|
+
createdAt: Date.now(),
|
|
91
|
+
updatedAt: Date.now(),
|
|
92
|
+
});
|
|
93
|
+
workspace = await ctx.db.get(workspaceId);
|
|
94
|
+
}
|
|
95
|
+
// REFERRAL DISABLED (2026-03-01): Risk of abuse with awesome-list exposure
|
|
96
|
+
// Tracking referredBy for analytics only, no credit bonus
|
|
97
|
+
if (isNewUser && referralCode) {
|
|
98
|
+
const referrer = await ctx.db
|
|
99
|
+
.query("workspaces")
|
|
100
|
+
.withIndex("by_referralCode", (q) => q.eq("referralCode", referralCode))
|
|
101
|
+
.first();
|
|
102
|
+
if (referrer && referrer._id !== workspace._id) {
|
|
103
|
+
// Track referral for analytics only
|
|
104
|
+
await ctx.db.patch(workspace._id, {
|
|
105
|
+
referredBy: referrer._id,
|
|
106
|
+
updatedAt: Date.now(),
|
|
107
|
+
});
|
|
108
|
+
// No credit bonus - referral rewards disabled
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Reuse existing session for same machine (fix: no more duplicate sessions per login)
|
|
112
|
+
const sessionToken = generateToken();
|
|
113
|
+
const userFingerprint2 = fingerprint || magicLink.sessionFingerprint;
|
|
114
|
+
const existingSession = userFingerprint2
|
|
115
|
+
? await ctx.db
|
|
116
|
+
.query("agentSessions")
|
|
117
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", workspace._id))
|
|
118
|
+
.filter((q) => q.eq(q.field("fingerprint"), userFingerprint2))
|
|
119
|
+
.first()
|
|
120
|
+
: null;
|
|
121
|
+
if (existingSession) {
|
|
122
|
+
// Refresh existing session instead of creating duplicate
|
|
123
|
+
await ctx.db.patch(existingSession._id, {
|
|
124
|
+
sessionToken,
|
|
125
|
+
lastUsedAt: Date.now(),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
await ctx.db.insert("agentSessions", {
|
|
130
|
+
workspaceId: workspace._id,
|
|
131
|
+
sessionToken,
|
|
132
|
+
fingerprint: userFingerprint2 || undefined,
|
|
133
|
+
lastUsedAt: Date.now(),
|
|
134
|
+
createdAt: Date.now(),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
// Link agent record to workspace (if agent exists for this fingerprint)
|
|
138
|
+
if (userFingerprint2) {
|
|
139
|
+
const agentForFingerprint = await ctx.db
|
|
140
|
+
.query("agents")
|
|
141
|
+
.filter((q) => q.eq(q.field("fingerprint"), userFingerprint2))
|
|
142
|
+
.first();
|
|
143
|
+
if (agentForFingerprint && !agentForFingerprint.workspaceId) {
|
|
144
|
+
await ctx.db.patch(agentForFingerprint._id, {
|
|
145
|
+
workspaceId: workspace._id,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Claim anonymous usage history
|
|
150
|
+
const userFingerprint = fingerprint || magicLink.sessionFingerprint;
|
|
151
|
+
if (userFingerprint) {
|
|
152
|
+
try {
|
|
153
|
+
// Find all analytics records with matching fingerprint and no workspaceId
|
|
154
|
+
const analyticsRecords = await ctx.db
|
|
155
|
+
.query("analytics")
|
|
156
|
+
.withIndex("by_identifier", (q) => q.eq("identifier", userFingerprint))
|
|
157
|
+
.collect();
|
|
158
|
+
// Filter to only unclaimed records
|
|
159
|
+
const unclaimedRecords = analyticsRecords.filter((r) => !r.workspaceId);
|
|
160
|
+
// Update each record to link it to the workspace
|
|
161
|
+
for (const record of unclaimedRecords) {
|
|
162
|
+
await ctx.db.patch(record._id, { workspaceId: workspace._id });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
// Non-critical error, just log it
|
|
167
|
+
console.error('Failed to claim anonymous usage:', err);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Notify Inbound Net (ALERTS) — async, non-blocking
|
|
171
|
+
await ctx.scheduler.runAfter(0, internal.inbound.notifySignup, {
|
|
172
|
+
email: workspace.email,
|
|
173
|
+
workspaceId: workspace._id,
|
|
174
|
+
tier: workspace.tier,
|
|
175
|
+
isNewUser,
|
|
176
|
+
timestamp: Date.now(),
|
|
177
|
+
});
|
|
178
|
+
return {
|
|
179
|
+
success: true,
|
|
180
|
+
sessionToken,
|
|
181
|
+
workspace: {
|
|
182
|
+
id: workspace._id,
|
|
183
|
+
email: workspace.email,
|
|
184
|
+
tier: workspace.tier,
|
|
185
|
+
referralCode: workspace.referralCode,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
// Get session from token
|
|
191
|
+
export const getSession = query({
|
|
192
|
+
args: { token: v.string() },
|
|
193
|
+
handler: async (ctx, { token }) => {
|
|
194
|
+
const session = await ctx.db
|
|
195
|
+
.query("agentSessions")
|
|
196
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
|
|
197
|
+
.first();
|
|
198
|
+
if (!session) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const workspace = await ctx.db.get(session.workspaceId);
|
|
202
|
+
if (!workspace)
|
|
203
|
+
return null;
|
|
204
|
+
return {
|
|
205
|
+
workspaceId: workspace._id,
|
|
206
|
+
email: workspace.email,
|
|
207
|
+
tier: workspace.tier,
|
|
208
|
+
status: workspace.status,
|
|
209
|
+
usageCount: workspace.usageCount,
|
|
210
|
+
usageLimit: workspace.usageLimit,
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
// ============================================
|
|
215
|
+
// DASHBOARD QUERIES
|
|
216
|
+
// ============================================
|
|
217
|
+
// Get full workspace dashboard data
|
|
218
|
+
export const getWorkspaceDashboard = query({
|
|
219
|
+
args: { token: v.string() },
|
|
220
|
+
handler: async (ctx, { token }) => {
|
|
221
|
+
// Verify session
|
|
222
|
+
const session = await ctx.db
|
|
223
|
+
.query("agentSessions")
|
|
224
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
|
|
225
|
+
.first();
|
|
226
|
+
if (!session) {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
// Note: lastUsedAt is updated via touchSession mutation separately
|
|
230
|
+
const workspace = await ctx.db.get(session.workspaceId);
|
|
231
|
+
if (!workspace)
|
|
232
|
+
return null;
|
|
233
|
+
// Get all agent sessions for this workspace
|
|
234
|
+
const agentSessions = await ctx.db
|
|
235
|
+
.query("agentSessions")
|
|
236
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", session.workspaceId))
|
|
237
|
+
.collect();
|
|
238
|
+
// Count agents: 1 main agent (if exists) + subagents
|
|
239
|
+
const hasMainAgent = workspace.mainAgentId ? 1 : 0;
|
|
240
|
+
const subagents = await ctx.db
|
|
241
|
+
.query("subagents")
|
|
242
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", session.workspaceId))
|
|
243
|
+
.collect();
|
|
244
|
+
const totalAgentCount = hasMainAgent + subagents.length;
|
|
245
|
+
// Get usage logs for this workspace (via agent credits or purchases)
|
|
246
|
+
const credits = await ctx.db
|
|
247
|
+
.query("agentCredits")
|
|
248
|
+
.collect();
|
|
249
|
+
// Filter credits that belong to this workspace's agents
|
|
250
|
+
const workspaceCredits = credits.filter(c => agentSessions.some(s => c.agentId === s.sessionToken));
|
|
251
|
+
// Get purchases for workspace agents
|
|
252
|
+
const purchases = await ctx.db
|
|
253
|
+
.query("purchases")
|
|
254
|
+
.collect();
|
|
255
|
+
const workspacePurchases = purchases.filter(p => agentSessions.some(s => p.agentId === s.sessionToken));
|
|
256
|
+
// Calculate usage remaining — backer tier is unlimited
|
|
257
|
+
const now = Date.now();
|
|
258
|
+
const isBackerTier = workspace.tier === "backer" || workspace.tier === "founder" ||
|
|
259
|
+
(workspace.backerUntil && workspace.backerUntil > now);
|
|
260
|
+
const effectiveLimit = isBackerTier ? -1 : workspace.usageLimit; // -1 = unlimited
|
|
261
|
+
const usageRemaining = isBackerTier ? -1 : Math.max(0, workspace.usageLimit - workspace.usageCount);
|
|
262
|
+
const usagePercentage = isBackerTier ? 0 : (workspace.usageCount / workspace.usageLimit) * 100;
|
|
263
|
+
// Budget status (PRD 2.6)
|
|
264
|
+
const monthStart = getMonthStartForBudget();
|
|
265
|
+
let currentSpend = workspace.monthlySpendCents || 0;
|
|
266
|
+
if (!workspace.lastSpendResetAt || workspace.lastSpendResetAt < monthStart) {
|
|
267
|
+
currentSpend = 0;
|
|
268
|
+
}
|
|
269
|
+
const budgetCap = workspace.budgetCap || null;
|
|
270
|
+
return {
|
|
271
|
+
workspace: {
|
|
272
|
+
id: workspace._id,
|
|
273
|
+
email: workspace.email,
|
|
274
|
+
workspaceName: workspace.workspaceName,
|
|
275
|
+
tier: workspace.tier,
|
|
276
|
+
status: workspace.status,
|
|
277
|
+
usageCount: workspace.usageCount,
|
|
278
|
+
usageLimit: effectiveLimit,
|
|
279
|
+
usageRemaining,
|
|
280
|
+
usagePercentage,
|
|
281
|
+
stripeCustomerId: workspace.stripeCustomerId,
|
|
282
|
+
createdAt: workspace.createdAt,
|
|
283
|
+
},
|
|
284
|
+
stats: {
|
|
285
|
+
totalAgents: totalAgentCount,
|
|
286
|
+
totalCredits: workspaceCredits.reduce((sum, c) => sum + c.balanceUsd, 0),
|
|
287
|
+
totalPurchases: workspacePurchases.length,
|
|
288
|
+
},
|
|
289
|
+
budget: {
|
|
290
|
+
budgetCapCents: budgetCap,
|
|
291
|
+
budgetCapUsd: budgetCap ? budgetCap / 100 : null,
|
|
292
|
+
currentSpendCents: currentSpend,
|
|
293
|
+
currentSpendUsd: currentSpend / 100,
|
|
294
|
+
remainingCents: budgetCap ? Math.max(0, budgetCap - currentSpend) : null,
|
|
295
|
+
remainingUsd: budgetCap ? Math.max(0, (budgetCap - currentSpend) / 100) : null,
|
|
296
|
+
budgetPercentage: budgetCap ? Math.min(100, (currentSpend / budgetCap) * 100) : null,
|
|
297
|
+
pauseOnBudgetExceeded: workspace.pauseOnBudgetExceeded || false,
|
|
298
|
+
isOverBudget: budgetCap ? currentSpend >= budgetCap : false,
|
|
299
|
+
isNearBudget: budgetCap ? currentSpend >= budgetCap * 0.8 : false,
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
// Helper for budget month start
|
|
305
|
+
function getMonthStartForBudget() {
|
|
306
|
+
const now = new Date();
|
|
307
|
+
return new Date(now.getUTCFullYear(), now.getUTCMonth(), 1, 0, 0, 0, 0).getTime();
|
|
308
|
+
}
|
|
309
|
+
// Get connected agents for workspace
|
|
310
|
+
export const getConnectedAgents = query({
|
|
311
|
+
args: { token: v.string() },
|
|
312
|
+
handler: async (ctx, { token }) => {
|
|
313
|
+
const session = await ctx.db
|
|
314
|
+
.query("agentSessions")
|
|
315
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
|
|
316
|
+
.first();
|
|
317
|
+
if (!session) {
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
const agentSessions = await ctx.db
|
|
321
|
+
.query("agentSessions")
|
|
322
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", session.workspaceId))
|
|
323
|
+
.collect();
|
|
324
|
+
return agentSessions.map((s) => ({
|
|
325
|
+
id: s._id,
|
|
326
|
+
fingerprint: s.fingerprint || "Unknown",
|
|
327
|
+
customName: s.customName || null,
|
|
328
|
+
name: s.customName || s.fingerprint || "Unknown",
|
|
329
|
+
lastUsedAt: s.lastUsedAt,
|
|
330
|
+
createdAt: s.createdAt,
|
|
331
|
+
isCurrent: s.sessionToken === token,
|
|
332
|
+
}));
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
// Admin: Delete session by ID (for cleanup)
|
|
336
|
+
export const adminDeleteSession = mutation({
|
|
337
|
+
args: { sessionId: v.id("agentSessions") },
|
|
338
|
+
handler: async (ctx, { sessionId }) => {
|
|
339
|
+
await ctx.db.delete(sessionId);
|
|
340
|
+
return { success: true };
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
// Debug: Get sessions by workspace email
|
|
344
|
+
export const getSessionsByEmail = query({
|
|
345
|
+
args: { email: v.string() },
|
|
346
|
+
handler: async (ctx, { email }) => {
|
|
347
|
+
const workspace = await ctx.db
|
|
348
|
+
.query("workspaces")
|
|
349
|
+
.withIndex("by_email", (q) => q.eq("email", email.toLowerCase()))
|
|
350
|
+
.first();
|
|
351
|
+
if (!workspace) {
|
|
352
|
+
return { error: "Workspace not found", sessions: [] };
|
|
353
|
+
}
|
|
354
|
+
const sessions = await ctx.db
|
|
355
|
+
.query("agentSessions")
|
|
356
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", workspace._id))
|
|
357
|
+
.collect();
|
|
358
|
+
return {
|
|
359
|
+
workspaceId: workspace._id,
|
|
360
|
+
email: workspace.email,
|
|
361
|
+
sessions: sessions.map(s => ({
|
|
362
|
+
id: s._id,
|
|
363
|
+
fingerprint: s.fingerprint,
|
|
364
|
+
createdAt: s.createdAt,
|
|
365
|
+
lastUsedAt: s.lastUsedAt,
|
|
366
|
+
})),
|
|
367
|
+
};
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
// Rename an agent session
|
|
371
|
+
export const renameAgent = mutation({
|
|
372
|
+
args: {
|
|
373
|
+
token: v.string(),
|
|
374
|
+
sessionId: v.id("agentSessions"),
|
|
375
|
+
name: v.string(),
|
|
376
|
+
},
|
|
377
|
+
handler: async (ctx, { token, sessionId, name }) => {
|
|
378
|
+
// Verify the requesting session
|
|
379
|
+
const session = await ctx.db
|
|
380
|
+
.query("agentSessions")
|
|
381
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
|
|
382
|
+
.first();
|
|
383
|
+
if (!session) {
|
|
384
|
+
throw new Error("Invalid session");
|
|
385
|
+
}
|
|
386
|
+
// Get the session to rename
|
|
387
|
+
const targetSession = await ctx.db.get(sessionId);
|
|
388
|
+
if (!targetSession || targetSession.workspaceId !== session.workspaceId) {
|
|
389
|
+
throw new Error("Session not found or access denied");
|
|
390
|
+
}
|
|
391
|
+
// Update the name (stored as customName field)
|
|
392
|
+
await ctx.db.patch(sessionId, { customName: name });
|
|
393
|
+
return { success: true };
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
// Get usage breakdown by provider
|
|
397
|
+
export const getUsageBreakdown = query({
|
|
398
|
+
args: { token: v.string() },
|
|
399
|
+
handler: async (ctx, { token }) => {
|
|
400
|
+
const session = await ctx.db
|
|
401
|
+
.query("agentSessions")
|
|
402
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
|
|
403
|
+
.first();
|
|
404
|
+
if (!session) {
|
|
405
|
+
return { byProvider: [], byDay: [], total: 0 };
|
|
406
|
+
}
|
|
407
|
+
// Get all sessions for this workspace
|
|
408
|
+
const agentSessions = await ctx.db
|
|
409
|
+
.query("agentSessions")
|
|
410
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", session.workspaceId))
|
|
411
|
+
.collect();
|
|
412
|
+
const sessionTokens = agentSessions.map(s => s.sessionToken);
|
|
413
|
+
// Get purchases for these agents
|
|
414
|
+
const allPurchases = await ctx.db.query("purchases").collect();
|
|
415
|
+
const workspacePurchases = allPurchases.filter(p => sessionTokens.includes(p.agentId));
|
|
416
|
+
// Get usage for purchases
|
|
417
|
+
const allUsage = await ctx.db.query("usage").collect();
|
|
418
|
+
const purchaseIds = workspacePurchases.map(p => p._id);
|
|
419
|
+
const workspaceUsage = allUsage.filter(u => purchaseIds.includes(u.purchaseId));
|
|
420
|
+
// Aggregate by provider
|
|
421
|
+
const byProvider = {};
|
|
422
|
+
for (const usage of workspaceUsage) {
|
|
423
|
+
if (!byProvider[usage.providerId]) {
|
|
424
|
+
byProvider[usage.providerId] = { calls: 0, cost: 0 };
|
|
425
|
+
}
|
|
426
|
+
byProvider[usage.providerId].calls += usage.unitsUsed;
|
|
427
|
+
byProvider[usage.providerId].cost += usage.costIncurredUsd;
|
|
428
|
+
}
|
|
429
|
+
// Aggregate by day (last 14 days)
|
|
430
|
+
const now = Date.now();
|
|
431
|
+
const fourteenDaysAgo = now - 14 * 24 * 60 * 60 * 1000;
|
|
432
|
+
const byDay = {};
|
|
433
|
+
for (const usage of workspaceUsage) {
|
|
434
|
+
if (usage.lastUsedAt >= fourteenDaysAgo) {
|
|
435
|
+
const day = new Date(usage.lastUsedAt).toISOString().split("T")[0];
|
|
436
|
+
byDay[day] = (byDay[day] || 0) + usage.unitsUsed;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
byProvider: Object.entries(byProvider).map(([provider, data]) => ({
|
|
441
|
+
provider,
|
|
442
|
+
calls: data.calls,
|
|
443
|
+
cost: data.cost,
|
|
444
|
+
})),
|
|
445
|
+
byDay: Object.entries(byDay)
|
|
446
|
+
.map(([date, calls]) => ({ date, calls }))
|
|
447
|
+
.sort((a, b) => a.date.localeCompare(b.date)),
|
|
448
|
+
total: workspaceUsage.reduce((sum, u) => sum + u.unitsUsed, 0),
|
|
449
|
+
};
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
// ============================================
|
|
453
|
+
// AGENT MANAGEMENT
|
|
454
|
+
// ============================================
|
|
455
|
+
// Revoke an agent session
|
|
456
|
+
export const revokeAgentSession = mutation({
|
|
457
|
+
args: {
|
|
458
|
+
token: v.string(),
|
|
459
|
+
sessionId: v.id("agentSessions"),
|
|
460
|
+
},
|
|
461
|
+
handler: async (ctx, { token, sessionId }) => {
|
|
462
|
+
// Verify the requesting session
|
|
463
|
+
const session = await ctx.db
|
|
464
|
+
.query("agentSessions")
|
|
465
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
|
|
466
|
+
.first();
|
|
467
|
+
if (!session) {
|
|
468
|
+
throw new Error("Unauthorized");
|
|
469
|
+
}
|
|
470
|
+
// Get the session to revoke
|
|
471
|
+
const targetSession = await ctx.db.get(sessionId);
|
|
472
|
+
if (!targetSession) {
|
|
473
|
+
throw new Error("Session not found");
|
|
474
|
+
}
|
|
475
|
+
// Verify same workspace
|
|
476
|
+
if (targetSession.workspaceId !== session.workspaceId) {
|
|
477
|
+
throw new Error("Unauthorized");
|
|
478
|
+
}
|
|
479
|
+
// Prevent revoking current session
|
|
480
|
+
if (targetSession.sessionToken === token) {
|
|
481
|
+
throw new Error("Cannot revoke current session");
|
|
482
|
+
}
|
|
483
|
+
// Delete the session
|
|
484
|
+
await ctx.db.delete(sessionId);
|
|
485
|
+
return { success: true };
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
// Logout (delete current session)
|
|
489
|
+
export const logout = mutation({
|
|
490
|
+
args: { token: v.string() },
|
|
491
|
+
handler: async (ctx, { token }) => {
|
|
492
|
+
const session = await ctx.db
|
|
493
|
+
.query("agentSessions")
|
|
494
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
|
|
495
|
+
.first();
|
|
496
|
+
if (session) {
|
|
497
|
+
await ctx.db.delete(session._id);
|
|
498
|
+
}
|
|
499
|
+
return { success: true };
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
// ============================================
|
|
503
|
+
// WORKSPACE MANAGEMENT
|
|
504
|
+
// ============================================
|
|
505
|
+
// Update workspace tier (for Stripe webhooks)
|
|
506
|
+
export const updateTier = mutation({
|
|
507
|
+
args: {
|
|
508
|
+
workspaceId: v.id("workspaces"),
|
|
509
|
+
tier: v.string(),
|
|
510
|
+
usageLimit: v.number(),
|
|
511
|
+
stripeCustomerId: v.optional(v.string()),
|
|
512
|
+
},
|
|
513
|
+
handler: async (ctx, { workspaceId, tier, usageLimit, stripeCustomerId }) => {
|
|
514
|
+
const updates = {
|
|
515
|
+
tier,
|
|
516
|
+
usageLimit,
|
|
517
|
+
updatedAt: Date.now(),
|
|
518
|
+
};
|
|
519
|
+
if (stripeCustomerId) {
|
|
520
|
+
updates.stripeCustomerId = stripeCustomerId;
|
|
521
|
+
}
|
|
522
|
+
await ctx.db.patch(workspaceId, updates);
|
|
523
|
+
return { success: true };
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
// Increment usage count
|
|
527
|
+
// Constants for rate limiting
|
|
528
|
+
const FREE_WEEKLY_LIMIT = 50;
|
|
529
|
+
const FREE_HOURLY_LIMIT = 10;
|
|
530
|
+
const BACKER_END_DATE = new Date("2026-12-31T23:59:59Z").getTime();
|
|
531
|
+
// Helper: Get start of current week (Monday 00:00 UTC)
|
|
532
|
+
function getWeekStart() {
|
|
533
|
+
const now = new Date();
|
|
534
|
+
const dayOfWeek = now.getUTCDay();
|
|
535
|
+
const diff = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // Monday = 0
|
|
536
|
+
const monday = new Date(now);
|
|
537
|
+
monday.setUTCDate(now.getUTCDate() - diff);
|
|
538
|
+
monday.setUTCHours(0, 0, 0, 0);
|
|
539
|
+
return monday.getTime();
|
|
540
|
+
}
|
|
541
|
+
// Helper: Get start of current hour
|
|
542
|
+
function getHourStart() {
|
|
543
|
+
const now = new Date();
|
|
544
|
+
now.setUTCMinutes(0, 0, 0);
|
|
545
|
+
return now.getTime();
|
|
546
|
+
}
|
|
547
|
+
export const incrementUsage = mutation({
|
|
548
|
+
args: {
|
|
549
|
+
workspaceId: v.id("workspaces"),
|
|
550
|
+
amount: v.optional(v.number()),
|
|
551
|
+
},
|
|
552
|
+
handler: async (ctx, { workspaceId, amount = 1 }) => {
|
|
553
|
+
const workspace = await ctx.db.get(workspaceId);
|
|
554
|
+
if (!workspace) {
|
|
555
|
+
throw new Error("Workspace not found");
|
|
556
|
+
}
|
|
557
|
+
const now = Date.now();
|
|
558
|
+
const weekStart = getWeekStart();
|
|
559
|
+
const hourStart = getHourStart();
|
|
560
|
+
// Check if Backer (unlimited until end of 2026)
|
|
561
|
+
const isBacker = workspace.tier === "backer" || workspace.tier === "founder" ||
|
|
562
|
+
(workspace.backerUntil && workspace.backerUntil > now);
|
|
563
|
+
// Initialize weekly/hourly counters if needed
|
|
564
|
+
let weeklyCount = workspace.weeklyUsageCount || 0;
|
|
565
|
+
let hourlyCount = workspace.hourlyUsageCount || 0;
|
|
566
|
+
// Reset weekly counter if new week
|
|
567
|
+
if (!workspace.lastWeeklyResetAt || workspace.lastWeeklyResetAt < weekStart) {
|
|
568
|
+
weeklyCount = 0;
|
|
569
|
+
}
|
|
570
|
+
// Reset hourly counter if new hour
|
|
571
|
+
if (!workspace.lastHourlyResetAt || workspace.lastHourlyResetAt < hourStart) {
|
|
572
|
+
hourlyCount = 0;
|
|
573
|
+
}
|
|
574
|
+
// Check rate limits for free tier
|
|
575
|
+
if (!isBacker && workspace.tier !== "pro" && workspace.tier !== "enterprise") {
|
|
576
|
+
// Check hourly limit (10/hour for free)
|
|
577
|
+
if (hourlyCount + amount > FREE_HOURLY_LIMIT) {
|
|
578
|
+
throw new Error(`Hourly rate limit exceeded (${FREE_HOURLY_LIMIT}/hour). Upgrade to Backer for unlimited.`);
|
|
579
|
+
}
|
|
580
|
+
// Check weekly limit (50/week for free)
|
|
581
|
+
if (weeklyCount + amount > FREE_WEEKLY_LIMIT) {
|
|
582
|
+
throw new Error(`Weekly limit exceeded (${FREE_WEEKLY_LIMIT}/week). Upgrade to Backer for unlimited.`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
const newTotalCount = workspace.usageCount + amount;
|
|
586
|
+
const newWeeklyCount = weeklyCount + amount;
|
|
587
|
+
const newHourlyCount = hourlyCount + amount;
|
|
588
|
+
await ctx.db.patch(workspaceId, {
|
|
589
|
+
usageCount: newTotalCount,
|
|
590
|
+
weeklyUsageCount: newWeeklyCount,
|
|
591
|
+
hourlyUsageCount: newHourlyCount,
|
|
592
|
+
lastWeeklyResetAt: weekStart,
|
|
593
|
+
lastHourlyResetAt: hourStart,
|
|
594
|
+
updatedAt: now,
|
|
595
|
+
});
|
|
596
|
+
// Calculate remaining for free tier
|
|
597
|
+
const weeklyRemaining = isBacker ? Infinity : Math.max(0, FREE_WEEKLY_LIMIT - newWeeklyCount);
|
|
598
|
+
const hourlyRemaining = isBacker ? Infinity : Math.max(0, FREE_HOURLY_LIMIT - newHourlyCount);
|
|
599
|
+
return {
|
|
600
|
+
success: true,
|
|
601
|
+
usageCount: newTotalCount,
|
|
602
|
+
weeklyUsageCount: newWeeklyCount,
|
|
603
|
+
weeklyRemaining,
|
|
604
|
+
hourlyRemaining,
|
|
605
|
+
isBacker,
|
|
606
|
+
};
|
|
607
|
+
},
|
|
608
|
+
});
|
|
609
|
+
// ============================================
|
|
610
|
+
// POLLING & VERIFICATION ENDPOINTS (for HTTP API)
|
|
611
|
+
// ============================================
|
|
612
|
+
// Poll magic link status (for agents to check if user clicked)
|
|
613
|
+
export const pollMagicLink = query({
|
|
614
|
+
args: { token: v.string() },
|
|
615
|
+
handler: async (ctx, { token }) => {
|
|
616
|
+
const magicLink = await ctx.db
|
|
617
|
+
.query("workspaceMagicLinks")
|
|
618
|
+
.withIndex("by_token", (q) => q.eq("token", token))
|
|
619
|
+
.first();
|
|
620
|
+
if (!magicLink) {
|
|
621
|
+
return { status: "not_found" };
|
|
622
|
+
}
|
|
623
|
+
const now = Date.now();
|
|
624
|
+
if (magicLink.usedAt) {
|
|
625
|
+
// Get the workspace and session
|
|
626
|
+
const workspace = await ctx.db
|
|
627
|
+
.query("workspaces")
|
|
628
|
+
.withIndex("by_email", (q) => q.eq("email", magicLink.email))
|
|
629
|
+
.first();
|
|
630
|
+
// Get the latest session for this workspace
|
|
631
|
+
const session = workspace
|
|
632
|
+
? await ctx.db
|
|
633
|
+
.query("agentSessions")
|
|
634
|
+
.withIndex("by_workspaceId", (q) => q.eq("workspaceId", workspace._id))
|
|
635
|
+
.order("desc")
|
|
636
|
+
.first()
|
|
637
|
+
: null;
|
|
638
|
+
return {
|
|
639
|
+
status: "verified",
|
|
640
|
+
workspace: workspace
|
|
641
|
+
? {
|
|
642
|
+
id: workspace._id,
|
|
643
|
+
email: workspace.email,
|
|
644
|
+
tier: workspace.tier,
|
|
645
|
+
usageCount: workspace.usageCount,
|
|
646
|
+
usageLimit: workspace.usageLimit,
|
|
647
|
+
}
|
|
648
|
+
: null,
|
|
649
|
+
sessionToken: session?.sessionToken,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
if (magicLink.expiresAt < now) {
|
|
653
|
+
return { status: "expired" };
|
|
654
|
+
}
|
|
655
|
+
return {
|
|
656
|
+
status: "pending",
|
|
657
|
+
expiresAt: magicLink.expiresAt,
|
|
658
|
+
};
|
|
659
|
+
},
|
|
660
|
+
});
|
|
661
|
+
// Verify session token (for HTTP API)
|
|
662
|
+
export const verifySession = query({
|
|
663
|
+
args: { sessionToken: v.string() },
|
|
664
|
+
handler: async (ctx, { sessionToken }) => {
|
|
665
|
+
const session = await ctx.db
|
|
666
|
+
.query("agentSessions")
|
|
667
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", sessionToken))
|
|
668
|
+
.first();
|
|
669
|
+
if (!session) {
|
|
670
|
+
return null;
|
|
671
|
+
}
|
|
672
|
+
const workspace = await ctx.db.get(session.workspaceId);
|
|
673
|
+
if (!workspace || workspace.status !== "active") {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
return {
|
|
677
|
+
workspaceId: workspace._id,
|
|
678
|
+
email: workspace.email,
|
|
679
|
+
tier: workspace.tier,
|
|
680
|
+
usageCount: workspace.usageCount,
|
|
681
|
+
usageLimit: workspace.usageLimit,
|
|
682
|
+
};
|
|
683
|
+
},
|
|
684
|
+
});
|
|
685
|
+
// Get workspace by email (for HTTP API)
|
|
686
|
+
export const getByEmail = query({
|
|
687
|
+
args: { email: v.string() },
|
|
688
|
+
handler: async (ctx, { email }) => {
|
|
689
|
+
const workspace = await ctx.db
|
|
690
|
+
.query("workspaces")
|
|
691
|
+
.withIndex("by_email", (q) => q.eq("email", email.toLowerCase()))
|
|
692
|
+
.first();
|
|
693
|
+
if (!workspace) {
|
|
694
|
+
return null;
|
|
695
|
+
}
|
|
696
|
+
return {
|
|
697
|
+
id: workspace._id,
|
|
698
|
+
email: workspace.email,
|
|
699
|
+
status: workspace.status,
|
|
700
|
+
tier: workspace.tier,
|
|
701
|
+
usageCount: workspace.usageCount,
|
|
702
|
+
usageLimit: workspace.usageLimit,
|
|
703
|
+
};
|
|
704
|
+
},
|
|
705
|
+
});
|
|
706
|
+
// Touch session (update lastUsedAt)
|
|
707
|
+
export const touchSession = mutation({
|
|
708
|
+
args: { sessionToken: v.string() },
|
|
709
|
+
handler: async (ctx, { sessionToken }) => {
|
|
710
|
+
const session = await ctx.db
|
|
711
|
+
.query("agentSessions")
|
|
712
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", sessionToken))
|
|
713
|
+
.first();
|
|
714
|
+
if (session) {
|
|
715
|
+
await ctx.db.patch(session._id, { lastUsedAt: Date.now() });
|
|
716
|
+
}
|
|
717
|
+
},
|
|
718
|
+
});
|
|
719
|
+
// ============================================
|
|
720
|
+
// MCP WORKSPACE FUNCTIONS
|
|
721
|
+
// ============================================
|
|
722
|
+
// Create a new workspace (called from MCP register_owner)
|
|
723
|
+
export const createWorkspace = mutation({
|
|
724
|
+
args: { email: v.string() },
|
|
725
|
+
handler: async (ctx, { email }) => {
|
|
726
|
+
const normalizedEmail = email.toLowerCase().trim();
|
|
727
|
+
// Check if workspace exists
|
|
728
|
+
const existing = await ctx.db
|
|
729
|
+
.query("workspaces")
|
|
730
|
+
.withIndex("by_email", (q) => q.eq("email", normalizedEmail))
|
|
731
|
+
.first();
|
|
732
|
+
if (existing) {
|
|
733
|
+
return {
|
|
734
|
+
success: false,
|
|
735
|
+
error: "workspace_exists",
|
|
736
|
+
workspaceId: existing._id,
|
|
737
|
+
status: existing.status,
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
// Create new workspace
|
|
741
|
+
const workspaceId = await ctx.db.insert("workspaces", {
|
|
742
|
+
email: normalizedEmail,
|
|
743
|
+
status: "pending",
|
|
744
|
+
tier: "free",
|
|
745
|
+
usageCount: 0,
|
|
746
|
+
usageLimit: 50, // Free tier limit
|
|
747
|
+
createdAt: Date.now(),
|
|
748
|
+
updatedAt: Date.now(),
|
|
749
|
+
});
|
|
750
|
+
return { success: true, workspaceId };
|
|
751
|
+
},
|
|
752
|
+
});
|
|
753
|
+
// Update workspace name
|
|
754
|
+
export const updateWorkspaceName = mutation({
|
|
755
|
+
args: {
|
|
756
|
+
token: v.string(),
|
|
757
|
+
name: v.string(),
|
|
758
|
+
},
|
|
759
|
+
handler: async (ctx, { token, name }) => {
|
|
760
|
+
const session = await ctx.db
|
|
761
|
+
.query("agentSessions")
|
|
762
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
|
|
763
|
+
.first();
|
|
764
|
+
if (!session)
|
|
765
|
+
throw new Error("Invalid session");
|
|
766
|
+
const trimmed = name.trim();
|
|
767
|
+
if (trimmed.length < 1 || trimmed.length > 100) {
|
|
768
|
+
throw new Error("Name must be between 1 and 100 characters");
|
|
769
|
+
}
|
|
770
|
+
await ctx.db.patch(session.workspaceId, {
|
|
771
|
+
workspaceName: trimmed,
|
|
772
|
+
updatedAt: Date.now(),
|
|
773
|
+
});
|
|
774
|
+
return { success: true, name: trimmed };
|
|
775
|
+
},
|
|
776
|
+
});
|
|
777
|
+
// Set or update password
|
|
778
|
+
export const setPassword = mutation({
|
|
779
|
+
args: {
|
|
780
|
+
token: v.string(),
|
|
781
|
+
password: v.string(),
|
|
782
|
+
},
|
|
783
|
+
handler: async (ctx, { token, password }) => {
|
|
784
|
+
const session = await ctx.db
|
|
785
|
+
.query("agentSessions")
|
|
786
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", token))
|
|
787
|
+
.first();
|
|
788
|
+
if (!session)
|
|
789
|
+
throw new Error("Invalid session");
|
|
790
|
+
if (password.length < 8)
|
|
791
|
+
throw new Error("Password must be at least 8 characters");
|
|
792
|
+
// Simple hash using built-in crypto
|
|
793
|
+
const encoder = new TextEncoder();
|
|
794
|
+
const data = encoder.encode(password + "apiclaw-salt-v1");
|
|
795
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
796
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
797
|
+
const hashHex = hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
|
|
798
|
+
await ctx.db.patch(session.workspaceId, {
|
|
799
|
+
passwordHash: hashHex,
|
|
800
|
+
updatedAt: Date.now(),
|
|
801
|
+
});
|
|
802
|
+
return { success: true };
|
|
803
|
+
},
|
|
804
|
+
});
|
|
805
|
+
// Create agent session for workspace (called from MCP after verification)
|
|
806
|
+
export const createAgentSession = mutation({
|
|
807
|
+
args: {
|
|
808
|
+
workspaceId: v.id("workspaces"),
|
|
809
|
+
fingerprint: v.optional(v.string()),
|
|
810
|
+
},
|
|
811
|
+
handler: async (ctx, { workspaceId, fingerprint }) => {
|
|
812
|
+
const workspace = await ctx.db.get(workspaceId);
|
|
813
|
+
if (!workspace) {
|
|
814
|
+
return { success: false, error: "workspace_not_found" };
|
|
815
|
+
}
|
|
816
|
+
if (workspace.status !== "active") {
|
|
817
|
+
return { success: false, error: "workspace_not_active" };
|
|
818
|
+
}
|
|
819
|
+
const sessionToken = "apiclaw_" + generateToken();
|
|
820
|
+
await ctx.db.insert("agentSessions", {
|
|
821
|
+
workspaceId,
|
|
822
|
+
sessionToken,
|
|
823
|
+
fingerprint,
|
|
824
|
+
lastUsedAt: Date.now(),
|
|
825
|
+
createdAt: Date.now(),
|
|
826
|
+
});
|
|
827
|
+
return { success: true, sessionToken };
|
|
828
|
+
},
|
|
829
|
+
});
|
|
830
|
+
// ============================================
|
|
831
|
+
// HELPER FUNCTIONS
|
|
832
|
+
// ============================================
|
|
833
|
+
function generateToken() {
|
|
834
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
835
|
+
let result = "";
|
|
836
|
+
for (let i = 0; i < 48; i++) {
|
|
837
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
838
|
+
}
|
|
839
|
+
return result;
|
|
840
|
+
}
|
|
841
|
+
// Get workspace status (for MCP check_workspace_status tool)
|
|
842
|
+
export const getWorkspaceStatus = query({
|
|
843
|
+
args: {
|
|
844
|
+
sessionToken: v.string(),
|
|
845
|
+
},
|
|
846
|
+
handler: async (ctx, args) => {
|
|
847
|
+
const session = await ctx.db
|
|
848
|
+
.query("agentSessions")
|
|
849
|
+
.withIndex("by_sessionToken", (q) => q.eq("sessionToken", args.sessionToken))
|
|
850
|
+
.first();
|
|
851
|
+
if (!session) {
|
|
852
|
+
return { authenticated: false };
|
|
853
|
+
}
|
|
854
|
+
const workspace = await ctx.db.get(session.workspaceId);
|
|
855
|
+
if (!workspace) {
|
|
856
|
+
return { authenticated: false };
|
|
857
|
+
}
|
|
858
|
+
const usageRemaining = workspace.usageLimit > 0
|
|
859
|
+
? workspace.usageLimit - workspace.usageCount
|
|
860
|
+
: -1; // -1 = unlimited
|
|
861
|
+
return {
|
|
862
|
+
authenticated: true,
|
|
863
|
+
email: workspace.email,
|
|
864
|
+
status: workspace.status,
|
|
865
|
+
tier: workspace.tier,
|
|
866
|
+
usageCount: workspace.usageCount,
|
|
867
|
+
usageLimit: workspace.usageLimit,
|
|
868
|
+
usageRemaining,
|
|
869
|
+
hasStripe: !!workspace.stripeCustomerId,
|
|
870
|
+
createdAt: workspace.createdAt,
|
|
871
|
+
};
|
|
872
|
+
},
|
|
873
|
+
});
|
|
874
|
+
// Admin functions for Hivr integration
|
|
875
|
+
export const adminActivateWorkspace = mutation({
|
|
876
|
+
args: { workspaceId: v.id("workspaces") },
|
|
877
|
+
handler: async (ctx, { workspaceId }) => {
|
|
878
|
+
const workspace = await ctx.db.get(workspaceId);
|
|
879
|
+
if (!workspace) {
|
|
880
|
+
return { success: false, error: "not_found" };
|
|
881
|
+
}
|
|
882
|
+
await ctx.db.patch(workspaceId, {
|
|
883
|
+
status: "active",
|
|
884
|
+
tier: "backer",
|
|
885
|
+
weeklyUsageLimit: 999999,
|
|
886
|
+
updatedAt: Date.now(),
|
|
887
|
+
});
|
|
888
|
+
return { success: true };
|
|
889
|
+
},
|
|
890
|
+
});
|
|
891
|
+
export const adminCreateSession = mutation({
|
|
892
|
+
args: { workspaceId: v.id("workspaces") },
|
|
893
|
+
handler: async (ctx, { workspaceId }) => {
|
|
894
|
+
const workspace = await ctx.db.get(workspaceId);
|
|
895
|
+
if (!workspace || workspace.status !== "active") {
|
|
896
|
+
return { success: false, error: "workspace_not_active" };
|
|
897
|
+
}
|
|
898
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
899
|
+
let token = '';
|
|
900
|
+
for (let i = 0; i < 32; i++) {
|
|
901
|
+
token += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
902
|
+
}
|
|
903
|
+
const sessionToken = "apiclaw_" + token;
|
|
904
|
+
await ctx.db.insert("agentSessions", {
|
|
905
|
+
workspaceId,
|
|
906
|
+
sessionToken,
|
|
907
|
+
fingerprint: "hivr-bees",
|
|
908
|
+
lastUsedAt: Date.now(),
|
|
909
|
+
createdAt: Date.now(),
|
|
910
|
+
});
|
|
911
|
+
return { success: true, sessionToken };
|
|
912
|
+
},
|
|
913
|
+
});
|
|
914
|
+
// TEMP: Admin query to debug workspace data
|
|
915
|
+
export const adminGetFullWorkspace = query({
|
|
916
|
+
args: { email: v.string() },
|
|
917
|
+
handler: async (ctx, { email }) => {
|
|
918
|
+
const workspace = await ctx.db
|
|
919
|
+
.query("workspaces")
|
|
920
|
+
.withIndex("by_email", (q) => q.eq("email", email.toLowerCase()))
|
|
921
|
+
.first();
|
|
922
|
+
if (!workspace) {
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
return {
|
|
926
|
+
_id: workspace._id,
|
|
927
|
+
email: workspace.email,
|
|
928
|
+
status: workspace.status,
|
|
929
|
+
tier: workspace.tier,
|
|
930
|
+
mainAgentId: workspace.mainAgentId || null,
|
|
931
|
+
mainAgentName: workspace.mainAgentName || null,
|
|
932
|
+
aiBackend: workspace.aiBackend || null,
|
|
933
|
+
usageCount: workspace.usageCount,
|
|
934
|
+
usageLimit: workspace.usageLimit,
|
|
935
|
+
createdAt: workspace.createdAt,
|
|
936
|
+
updatedAt: workspace.updatedAt,
|
|
937
|
+
};
|
|
938
|
+
},
|
|
939
|
+
});
|
|
940
|
+
/**
|
|
941
|
+
* Claim anonymous usage history when a user registers
|
|
942
|
+
* Links all analytics records with matching fingerprint to the workspace
|
|
943
|
+
*/
|
|
944
|
+
export const claimAnonymousUsage = mutation({
|
|
945
|
+
args: {
|
|
946
|
+
workspaceId: v.id("workspaces"),
|
|
947
|
+
machineFingerprint: v.string(),
|
|
948
|
+
},
|
|
949
|
+
handler: async (ctx, { workspaceId, machineFingerprint }) => {
|
|
950
|
+
// Verify workspace exists
|
|
951
|
+
const workspace = await ctx.db.get(workspaceId);
|
|
952
|
+
if (!workspace) {
|
|
953
|
+
return { success: false, error: "Workspace not found" };
|
|
954
|
+
}
|
|
955
|
+
// Find all analytics records with matching fingerprint and no workspaceId
|
|
956
|
+
const analyticsRecords = await ctx.db
|
|
957
|
+
.query("analytics")
|
|
958
|
+
.withIndex("by_identifier", (q) => q.eq("identifier", machineFingerprint))
|
|
959
|
+
.collect();
|
|
960
|
+
// Filter to only unclaimed records
|
|
961
|
+
const unclaimedRecords = analyticsRecords.filter((r) => !r.workspaceId);
|
|
962
|
+
// Update each record to link it to the workspace
|
|
963
|
+
let claimedCount = 0;
|
|
964
|
+
for (const record of unclaimedRecords) {
|
|
965
|
+
await ctx.db.patch(record._id, { workspaceId });
|
|
966
|
+
claimedCount++;
|
|
967
|
+
}
|
|
968
|
+
return {
|
|
969
|
+
success: true,
|
|
970
|
+
claimedCount,
|
|
971
|
+
message: `Claimed ${claimedCount} anonymous usage records`,
|
|
972
|
+
};
|
|
973
|
+
},
|
|
974
|
+
});
|
|
975
|
+
//# sourceMappingURL=workspaces.js.map
|