@nordsym/apiclaw 1.3.2 → 1.3.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 +6 -0
- package/convex/billing.ts +341 -0
- package/convex/email.ts +276 -0
- package/convex/http.ts +154 -0
- package/convex/schema.ts +43 -0
- package/convex/workspaces.ts +663 -0
- package/dist/index.js +387 -4
- package/dist/index.js.map +1 -1
- package/dist/open-apis.d.ts.map +1 -1
- package/dist/open-apis.js +12 -19
- package/dist/open-apis.js.map +1 -1
- package/dist/session.d.ts +29 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +87 -0
- package/dist/session.js.map +1 -0
- package/docs/PRD-agent-first-billing.md +525 -0
- package/landing/package-lock.json +21 -3
- package/landing/package.json +2 -1
- package/landing/src/app/api/stripe/webhook/route.ts +178 -0
- package/landing/src/app/api/workspace-auth/magic-link/route.ts +84 -0
- package/landing/src/app/api/workspace-auth/session/route.ts +73 -0
- package/landing/src/app/api/workspace-auth/verify/route.ts +57 -0
- package/landing/src/app/auth/verify/page.tsx +292 -0
- package/landing/src/app/dashboard/layout.tsx +22 -0
- package/landing/src/app/dashboard/page.tsx +692 -0
- package/landing/src/app/dashboard/verify/page.tsx +108 -0
- package/landing/src/app/login/page.tsx +204 -0
- package/landing/src/app/upgrade/page.tsx +288 -0
- package/landing/src/lib/stats.json +14 -15
- package/landing/src/middleware.ts +50 -0
- package/landing/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/index.ts +434 -4
- package/src/open-apis.ts +13 -21
- package/src/session.ts +103 -0
package/convex/http.ts
CHANGED
|
@@ -608,3 +608,157 @@ http.route({
|
|
|
608
608
|
method: "OPTIONS",
|
|
609
609
|
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
610
610
|
});
|
|
611
|
+
|
|
612
|
+
// ==============================================
|
|
613
|
+
// WORKSPACE / MAGIC LINK ENDPOINTS
|
|
614
|
+
// ==============================================
|
|
615
|
+
|
|
616
|
+
// Create magic link and send email
|
|
617
|
+
http.route({
|
|
618
|
+
path: "/workspace/magic-link",
|
|
619
|
+
method: "POST",
|
|
620
|
+
handler: httpAction(async (ctx, request) => {
|
|
621
|
+
try {
|
|
622
|
+
const body = await request.json();
|
|
623
|
+
const { email, fingerprint } = body;
|
|
624
|
+
|
|
625
|
+
if (!email || !email.includes("@")) {
|
|
626
|
+
return jsonResponse({ error: "Valid email required" }, 400);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Create magic link
|
|
630
|
+
const result = await ctx.runMutation(api.workspaces.createMagicLink, {
|
|
631
|
+
email: email.toLowerCase(),
|
|
632
|
+
fingerprint,
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
// Send email
|
|
636
|
+
await ctx.runAction(api.email.sendMagicLinkEmail, {
|
|
637
|
+
email: email.toLowerCase(),
|
|
638
|
+
token: result.token,
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
return jsonResponse({
|
|
642
|
+
success: true,
|
|
643
|
+
token: result.token,
|
|
644
|
+
expiresAt: result.expiresAt,
|
|
645
|
+
message: "Magic link sent! Check your email.",
|
|
646
|
+
});
|
|
647
|
+
} catch (e: any) {
|
|
648
|
+
console.error("Magic link error:", e);
|
|
649
|
+
return jsonResponse({ error: e.message || "Failed to create magic link" }, 500);
|
|
650
|
+
}
|
|
651
|
+
}),
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
http.route({
|
|
655
|
+
path: "/workspace/magic-link",
|
|
656
|
+
method: "OPTIONS",
|
|
657
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
// Poll magic link status (for agents to check if user clicked)
|
|
661
|
+
http.route({
|
|
662
|
+
path: "/workspace/poll",
|
|
663
|
+
method: "GET",
|
|
664
|
+
handler: httpAction(async (ctx, request) => {
|
|
665
|
+
const url = new URL(request.url);
|
|
666
|
+
const token = url.searchParams.get("token");
|
|
667
|
+
|
|
668
|
+
if (!token) {
|
|
669
|
+
return jsonResponse({ error: "token required" }, 400);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const result = await ctx.runQuery(api.workspaces.pollMagicLink, { token });
|
|
673
|
+
return jsonResponse(result);
|
|
674
|
+
}),
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
http.route({
|
|
678
|
+
path: "/workspace/poll",
|
|
679
|
+
method: "OPTIONS",
|
|
680
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
// Verify session token
|
|
684
|
+
http.route({
|
|
685
|
+
path: "/workspace/verify-session",
|
|
686
|
+
method: "GET",
|
|
687
|
+
handler: httpAction(async (ctx, request) => {
|
|
688
|
+
const url = new URL(request.url);
|
|
689
|
+
const sessionToken = url.searchParams.get("sessionToken");
|
|
690
|
+
|
|
691
|
+
if (!sessionToken) {
|
|
692
|
+
return jsonResponse({ error: "sessionToken required" }, 400);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const result = await ctx.runQuery(api.workspaces.verifySession, { sessionToken });
|
|
696
|
+
|
|
697
|
+
if (!result) {
|
|
698
|
+
return jsonResponse({ error: "Invalid or expired session" }, 401);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return jsonResponse(result);
|
|
702
|
+
}),
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
http.route({
|
|
706
|
+
path: "/workspace/verify-session",
|
|
707
|
+
method: "OPTIONS",
|
|
708
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
// Get workspace by email
|
|
712
|
+
http.route({
|
|
713
|
+
path: "/workspace/by-email",
|
|
714
|
+
method: "GET",
|
|
715
|
+
handler: httpAction(async (ctx, request) => {
|
|
716
|
+
const url = new URL(request.url);
|
|
717
|
+
const email = url.searchParams.get("email");
|
|
718
|
+
|
|
719
|
+
if (!email) {
|
|
720
|
+
return jsonResponse({ error: "email required" }, 400);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const result = await ctx.runQuery(api.workspaces.getByEmail, { email });
|
|
724
|
+
|
|
725
|
+
if (!result) {
|
|
726
|
+
return jsonResponse({ exists: false });
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return jsonResponse({ exists: true, workspace: result });
|
|
730
|
+
}),
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
http.route({
|
|
734
|
+
path: "/workspace/by-email",
|
|
735
|
+
method: "OPTIONS",
|
|
736
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
// Send reminder email
|
|
740
|
+
http.route({
|
|
741
|
+
path: "/workspace/send-reminder",
|
|
742
|
+
method: "POST",
|
|
743
|
+
handler: httpAction(async (ctx, request) => {
|
|
744
|
+
try {
|
|
745
|
+
const body = await request.json();
|
|
746
|
+
const { email, token } = body;
|
|
747
|
+
|
|
748
|
+
if (!email || !token) {
|
|
749
|
+
return jsonResponse({ error: "email and token required" }, 400);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
await ctx.runAction(api.email.sendReminderEmail, { email, token });
|
|
753
|
+
return jsonResponse({ success: true });
|
|
754
|
+
} catch (e: any) {
|
|
755
|
+
return jsonResponse({ error: e.message }, 500);
|
|
756
|
+
}
|
|
757
|
+
}),
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
http.route({
|
|
761
|
+
path: "/workspace/send-reminder",
|
|
762
|
+
method: "OPTIONS",
|
|
763
|
+
handler: httpAction(async () => new Response(null, { headers: corsHeaders })),
|
|
764
|
+
});
|
package/convex/schema.ts
CHANGED
|
@@ -37,6 +37,49 @@ export default defineSchema({
|
|
|
37
37
|
.index("by_purchaseId", ["purchaseId"])
|
|
38
38
|
.index("by_providerId", ["providerId"]),
|
|
39
39
|
|
|
40
|
+
// ============================================
|
|
41
|
+
// WORKSPACE TABLES (MCP Agent Authentication)
|
|
42
|
+
// ============================================
|
|
43
|
+
|
|
44
|
+
// Workspaces (agent owner accounts)
|
|
45
|
+
workspaces: defineTable({
|
|
46
|
+
email: v.string(),
|
|
47
|
+
passwordHash: v.optional(v.string()),
|
|
48
|
+
status: v.string(), // "pending" | "active" | "suspended"
|
|
49
|
+
tier: v.string(), // "free" | "pro" | "enterprise"
|
|
50
|
+
usageCount: v.number(), // total API calls made
|
|
51
|
+
usageLimit: v.number(), // max API calls for tier
|
|
52
|
+
stripeCustomerId: v.optional(v.string()),
|
|
53
|
+
createdAt: v.number(),
|
|
54
|
+
updatedAt: v.number(),
|
|
55
|
+
})
|
|
56
|
+
.index("by_email", ["email"])
|
|
57
|
+
.index("by_stripeCustomerId", ["stripeCustomerId"])
|
|
58
|
+
.index("by_status", ["status"]),
|
|
59
|
+
|
|
60
|
+
// Agent sessions (for MCP server authentication)
|
|
61
|
+
agentSessions: defineTable({
|
|
62
|
+
workspaceId: v.id("workspaces"),
|
|
63
|
+
sessionToken: v.string(),
|
|
64
|
+
fingerprint: v.optional(v.string()), // machine fingerprint
|
|
65
|
+
lastUsedAt: v.number(),
|
|
66
|
+
createdAt: v.number(),
|
|
67
|
+
})
|
|
68
|
+
.index("by_sessionToken", ["sessionToken"])
|
|
69
|
+
.index("by_workspaceId", ["workspaceId"]),
|
|
70
|
+
|
|
71
|
+
// Magic links for workspace email verification
|
|
72
|
+
workspaceMagicLinks: defineTable({
|
|
73
|
+
email: v.string(),
|
|
74
|
+
token: v.string(),
|
|
75
|
+
sessionFingerprint: v.optional(v.string()),
|
|
76
|
+
expiresAt: v.number(),
|
|
77
|
+
usedAt: v.optional(v.number()),
|
|
78
|
+
createdAt: v.number(),
|
|
79
|
+
})
|
|
80
|
+
.index("by_token", ["token"])
|
|
81
|
+
.index("by_email", ["email"]),
|
|
82
|
+
|
|
40
83
|
// Credit top-ups (from Stripe payments)
|
|
41
84
|
creditTopups: defineTable({
|
|
42
85
|
agentId: v.string(),
|