@nordsym/apiclaw 1.5.17 → 1.5.19

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