@nordsym/apiclaw 1.3.6 → 1.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +422 -169
- package/convex/_generated/api.d.ts +16 -0
- package/convex/agents.ts +403 -0
- package/convex/billing.ts +651 -216
- package/convex/crons.ts +17 -0
- package/convex/directCall.ts +80 -0
- package/convex/earnProgress.ts +753 -0
- package/convex/email.ts +135 -82
- package/convex/feedback.ts +265 -0
- package/convex/http.ts +80 -4
- package/convex/logs.ts +304 -0
- package/convex/providerKeys.ts +289 -0
- package/convex/providers.ts +18 -0
- package/convex/schema.ts +185 -1
- package/convex/stripeActions.ts +512 -0
- package/convex/webhooks.ts +494 -0
- package/convex/workspaces.ts +158 -3
- package/dist/adapters/base.d.ts +112 -0
- package/dist/adapters/base.d.ts.map +1 -0
- package/dist/adapters/base.js +247 -0
- package/dist/adapters/base.js.map +1 -0
- package/dist/adapters/claude-desktop.d.ts +12 -0
- package/dist/adapters/claude-desktop.d.ts.map +1 -0
- package/dist/adapters/claude-desktop.js +36 -0
- package/dist/adapters/claude-desktop.js.map +1 -0
- package/dist/adapters/cline.d.ts +20 -0
- package/dist/adapters/cline.d.ts.map +1 -0
- package/dist/adapters/cline.js +77 -0
- package/dist/adapters/cline.js.map +1 -0
- package/dist/adapters/continue.d.ts +26 -0
- package/dist/adapters/continue.d.ts.map +1 -0
- package/dist/adapters/continue.js +68 -0
- package/dist/adapters/continue.js.map +1 -0
- package/dist/adapters/cursor.d.ts +12 -0
- package/dist/adapters/cursor.d.ts.map +1 -0
- package/dist/adapters/cursor.js +38 -0
- package/dist/adapters/cursor.js.map +1 -0
- package/dist/adapters/custom.d.ts +47 -0
- package/dist/adapters/custom.d.ts.map +1 -0
- package/dist/adapters/custom.js +146 -0
- package/dist/adapters/custom.js.map +1 -0
- package/dist/adapters/detect.d.ts +69 -0
- package/dist/adapters/detect.d.ts.map +1 -0
- package/dist/adapters/detect.js +158 -0
- package/dist/adapters/detect.js.map +1 -0
- package/dist/adapters/index.d.ts +21 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +23 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/windsurf.d.ts +12 -0
- package/dist/adapters/windsurf.d.ts.map +1 -0
- package/dist/adapters/windsurf.js +39 -0
- package/dist/adapters/windsurf.js.map +1 -0
- package/dist/bin.d.ts +9 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +19 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +34 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +312 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/index.d.ts +9 -0
- package/dist/cli/commands/index.d.ts.map +1 -0
- package/dist/cli/commands/index.js +9 -0
- package/dist/cli/commands/index.js.map +1 -0
- package/dist/cli/commands/restore.d.ts +50 -0
- package/dist/cli/commands/restore.d.ts.map +1 -0
- package/dist/cli/commands/restore.js +260 -0
- package/dist/cli/commands/restore.js.map +1 -0
- package/dist/cli/commands/setup.d.ts +19 -0
- package/dist/cli/commands/setup.d.ts.map +1 -0
- package/dist/cli/commands/setup.js +206 -0
- package/dist/cli/commands/setup.js.map +1 -0
- package/dist/cli/commands/uninstall.d.ts +37 -0
- package/dist/cli/commands/uninstall.d.ts.map +1 -0
- package/dist/cli/commands/uninstall.js +189 -0
- package/dist/cli/commands/uninstall.js.map +1 -0
- package/dist/cli/index.d.ts +7 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +97 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/discovery.d.ts +6 -2
- package/dist/discovery.d.ts.map +1 -1
- package/dist/discovery.js +296 -2
- package/dist/discovery.js.map +1 -1
- package/dist/enterprise/env.d.ts +56 -0
- package/dist/enterprise/env.d.ts.map +1 -0
- package/dist/enterprise/env.js +124 -0
- package/dist/enterprise/env.js.map +1 -0
- package/dist/enterprise/index.d.ts +7 -0
- package/dist/enterprise/index.d.ts.map +1 -0
- package/dist/enterprise/index.js +7 -0
- package/dist/enterprise/index.js.map +1 -0
- package/dist/enterprise/script-generator.d.ts +32 -0
- package/dist/enterprise/script-generator.d.ts.map +1 -0
- package/dist/enterprise/script-generator.js +461 -0
- package/dist/enterprise/script-generator.js.map +1 -0
- package/dist/execute.d.ts +21 -0
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +231 -0
- package/dist/execute.js.map +1 -1
- package/dist/index.js +257 -7
- package/dist/index.js.map +1 -1
- package/dist/metered.d.ts +62 -0
- package/dist/metered.d.ts.map +1 -0
- package/dist/metered.js +81 -0
- package/dist/metered.js.map +1 -0
- package/dist/stripe.d.ts +62 -0
- package/dist/stripe.d.ts.map +1 -1
- package/dist/stripe.js +212 -0
- package/dist/stripe.js.map +1 -1
- package/dist/types.d.ts +29 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/ui/colors.d.ts +111 -0
- package/dist/ui/colors.d.ts.map +1 -0
- package/dist/ui/colors.js +185 -0
- package/dist/ui/colors.js.map +1 -0
- package/dist/ui/errors.d.ts +69 -0
- package/dist/ui/errors.d.ts.map +1 -0
- package/dist/ui/errors.js +334 -0
- package/dist/ui/errors.js.map +1 -0
- package/dist/ui/index.d.ts +10 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +14 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/prompts.d.ts +88 -0
- package/dist/ui/prompts.d.ts.map +1 -0
- package/dist/ui/prompts.js +295 -0
- package/dist/ui/prompts.js.map +1 -0
- package/dist/ui/spinner.d.ts +112 -0
- package/dist/ui/spinner.d.ts.map +1 -0
- package/dist/ui/spinner.js +229 -0
- package/dist/ui/spinner.js.map +1 -0
- package/dist/utils/backup.d.ts +48 -0
- package/dist/utils/backup.d.ts.map +1 -0
- package/dist/utils/backup.js +182 -0
- package/dist/utils/backup.js.map +1 -0
- package/dist/utils/config.d.ts +80 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +221 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/os.d.ts +45 -0
- package/dist/utils/os.d.ts.map +1 -0
- package/dist/utils/os.js +106 -0
- package/dist/utils/os.js.map +1 -0
- package/dist/utils/paths.d.ts +38 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +160 -0
- package/dist/utils/paths.js.map +1 -0
- package/docs/PRD-BILLING.md +226 -0
- package/docs/PRD-EARN-SYSTEM.md +261 -0
- package/docs/PRD-MCP-AUTO-SETUP.md +623 -0
- package/docs/PRD-final-polish.md +117 -0
- package/docs/PRD-mobile-responsive.md +56 -0
- package/docs/PRD-navigation-expansion.md +295 -0
- package/docs/PRD-stripe-billing.md +312 -0
- package/docs/PRD-workspace-cleanup.md +200 -0
- package/docs/enterprise-deployment.md +728 -0
- package/landing/next.config.mjs +14 -0
- package/landing/public/stats.json +4 -2
- package/landing/scripts/generate-stats.js +12 -0
- package/landing/src/app/api/billing/checkout/route.ts +109 -0
- package/landing/src/app/api/billing/payment-method/route.ts +118 -0
- package/landing/src/app/api/billing/portal/route.ts +64 -0
- package/landing/src/app/api/workspace-auth/magic-link/route.ts +6 -3
- package/landing/src/app/auth/verify/page.tsx +31 -9
- package/landing/src/app/docs/page.tsx +1 -1
- package/landing/src/app/earn/page.tsx +6 -6
- package/landing/src/app/join/page.tsx +49 -0
- package/landing/src/app/login/page.tsx +8 -2
- package/landing/src/app/page.tsx +81 -96
- package/landing/src/app/providers/dashboard/page.tsx +1 -1
- package/landing/src/app/providers/register/page.tsx +1 -1
- package/landing/src/app/workspace/page.tsx +3269 -534
- package/landing/src/components/CheckoutButton.tsx +188 -0
- package/landing/src/components/EarnCreditsTab.tsx +842 -0
- package/landing/src/components/Toast.tsx +84 -0
- package/landing/src/lib/stats.json +3 -1
- package/package.json +9 -2
- package/src/adapters/base.ts +363 -0
- package/src/adapters/claude-desktop.ts +41 -0
- package/src/adapters/cline.ts +88 -0
- package/src/adapters/continue.ts +91 -0
- package/src/adapters/cursor.ts +43 -0
- package/src/adapters/custom.ts +188 -0
- package/src/adapters/detect.ts +202 -0
- package/src/adapters/index.ts +47 -0
- package/src/adapters/windsurf.ts +44 -0
- package/src/bin.ts +19 -0
- package/src/cli/commands/doctor.ts +367 -0
- package/src/cli/commands/index.ts +9 -0
- package/src/cli/commands/restore.ts +333 -0
- package/src/cli/commands/setup.ts +276 -0
- package/src/cli/commands/uninstall.ts +240 -0
- package/src/cli/index.ts +107 -0
- package/src/discovery.ts +328 -3
- package/src/enterprise/env.ts +156 -0
- package/src/enterprise/index.ts +7 -0
- package/src/enterprise/script-generator.ts +481 -0
- package/src/execute.ts +256 -0
- package/src/index.ts +290 -7
- package/src/metered.ts +149 -0
- package/src/stripe.ts +253 -0
- package/src/types.ts +32 -0
- package/src/ui/colors.ts +219 -0
- package/src/ui/errors.ts +394 -0
- package/src/ui/index.ts +17 -0
- package/src/ui/prompts.ts +390 -0
- package/src/ui/spinner.ts +325 -0
- package/src/utils/backup.ts +224 -0
- package/src/utils/config.ts +315 -0
- package/src/utils/os.ts +124 -0
- package/src/utils/paths.ts +203 -0
- package/landing/tsconfig.tsbuildinfo +0 -1
package/landing/next.config.mjs
CHANGED
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
/** @type {import('next').NextConfig} */
|
|
2
2
|
const nextConfig = {
|
|
3
3
|
output: 'standalone',
|
|
4
|
+
async redirects() {
|
|
5
|
+
return [
|
|
6
|
+
{
|
|
7
|
+
source: '/docs',
|
|
8
|
+
destination: '/workspace?tab=docs',
|
|
9
|
+
permanent: false,
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
source: '/faq',
|
|
13
|
+
destination: '/#faq',
|
|
14
|
+
permanent: false,
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
},
|
|
4
18
|
};
|
|
5
19
|
|
|
6
20
|
export default nextConfig;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"apiCount": 22392,
|
|
3
|
+
"openApiCount": 996,
|
|
4
|
+
"directCallCount": 10,
|
|
3
5
|
"categoryCount": 14,
|
|
4
|
-
"lastUpdated": "2026-02-
|
|
5
|
-
"generatedAt": "2026-02-
|
|
6
|
+
"lastUpdated": "2026-02-28T21:45:00.000Z",
|
|
7
|
+
"generatedAt": "2026-02-28T21:45:00.000Z",
|
|
6
8
|
"categoryBreakdown": {
|
|
7
9
|
"Utilities": 7069,
|
|
8
10
|
"Analytics": 2600,
|
|
@@ -88,8 +88,18 @@ try {
|
|
|
88
88
|
});
|
|
89
89
|
const uniqueCategories = Object.keys(categoryBreakdown);
|
|
90
90
|
|
|
91
|
+
// Count open APIs (no auth required) - case insensitive
|
|
92
|
+
const openApiCount = registry.apis.filter(api =>
|
|
93
|
+
!api.auth || api.auth === '' || api.auth.toLowerCase() === 'none'
|
|
94
|
+
).length;
|
|
95
|
+
|
|
96
|
+
// Direct call providers (hardcoded for now, update when adding new providers)
|
|
97
|
+
const directCallCount = 10;
|
|
98
|
+
|
|
91
99
|
const stats = {
|
|
92
100
|
apiCount: registry.count,
|
|
101
|
+
openApiCount: openApiCount,
|
|
102
|
+
directCallCount: directCallCount,
|
|
93
103
|
categoryCount: uniqueCategories.length,
|
|
94
104
|
lastUpdated: registry.lastUpdated || new Date().toISOString().split('T')[0],
|
|
95
105
|
generatedAt: new Date().toISOString(),
|
|
@@ -109,6 +119,8 @@ try {
|
|
|
109
119
|
// Write fallback stats
|
|
110
120
|
const fallback = {
|
|
111
121
|
apiCount: 22392,
|
|
122
|
+
openApiCount: 996,
|
|
123
|
+
directCallCount: 10,
|
|
112
124
|
categoryCount: 14,
|
|
113
125
|
lastUpdated: new Date().toISOString().split('T')[0],
|
|
114
126
|
generatedAt: new Date().toISOString(),
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import Stripe from "stripe";
|
|
3
|
+
|
|
4
|
+
const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || "https://adventurous-avocet-799.convex.cloud";
|
|
5
|
+
const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
|
|
6
|
+
|
|
7
|
+
const stripe = stripeSecretKey ? new Stripe(stripeSecretKey) : null;
|
|
8
|
+
|
|
9
|
+
// Price ID for usage-based subscription
|
|
10
|
+
const USAGE_BASED_PRICE_ID = process.env.STRIPE_USAGE_PRICE_ID || "price_1R9hAUCQSPYXHCfhMmaPnmN9";
|
|
11
|
+
|
|
12
|
+
export async function POST(req: NextRequest) {
|
|
13
|
+
if (!stripe) {
|
|
14
|
+
return NextResponse.json({ error: "Stripe not configured" }, { status: 500 });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const { token } = await req.json();
|
|
19
|
+
|
|
20
|
+
if (!token) {
|
|
21
|
+
return NextResponse.json({ error: "Missing token" }, { status: 400 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Verify session and get workspace
|
|
25
|
+
const sessionRes = await fetch(`${CONVEX_URL}/api/query`, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: { "Content-Type": "application/json" },
|
|
28
|
+
body: JSON.stringify({
|
|
29
|
+
path: "workspaces:getSessionWorkspace",
|
|
30
|
+
args: { token },
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const sessionData = await sessionRes.json();
|
|
35
|
+
const workspace = sessionData.value || sessionData;
|
|
36
|
+
|
|
37
|
+
if (!workspace || !workspace._id) {
|
|
38
|
+
return NextResponse.json({ error: "Invalid session" }, { status: 401 });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check if customer already exists
|
|
42
|
+
let customerId = workspace.stripeCustomerId;
|
|
43
|
+
|
|
44
|
+
if (!customerId) {
|
|
45
|
+
// Create new Stripe customer
|
|
46
|
+
const customer = await stripe.customers.create({
|
|
47
|
+
email: workspace.email,
|
|
48
|
+
metadata: {
|
|
49
|
+
workspaceId: workspace._id,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
customerId = customer.id;
|
|
54
|
+
|
|
55
|
+
// Save customer ID to workspace
|
|
56
|
+
await fetch(`${CONVEX_URL}/api/mutation`, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: { "Content-Type": "application/json" },
|
|
59
|
+
body: JSON.stringify({
|
|
60
|
+
path: "billing:linkCustomer",
|
|
61
|
+
args: {
|
|
62
|
+
workspaceId: workspace._id,
|
|
63
|
+
stripeCustomerId: customerId,
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Determine the app URL for redirects
|
|
70
|
+
const appUrl = process.env.NEXT_PUBLIC_APP_URL || "https://apiclaw.com";
|
|
71
|
+
|
|
72
|
+
// Create Checkout Session for setup mode (to collect payment method)
|
|
73
|
+
// This will create a subscription with usage-based billing
|
|
74
|
+
const session = await stripe.checkout.sessions.create({
|
|
75
|
+
customer: customerId,
|
|
76
|
+
mode: "subscription",
|
|
77
|
+
payment_method_types: ["card"],
|
|
78
|
+
line_items: [
|
|
79
|
+
{
|
|
80
|
+
price: USAGE_BASED_PRICE_ID,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
subscription_data: {
|
|
84
|
+
metadata: {
|
|
85
|
+
workspaceId: workspace._id,
|
|
86
|
+
type: "usage_based",
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
metadata: {
|
|
90
|
+
workspaceId: workspace._id,
|
|
91
|
+
type: "usage_based",
|
|
92
|
+
},
|
|
93
|
+
success_url: `${appUrl}/workspace?billing=success`,
|
|
94
|
+
cancel_url: `${appUrl}/workspace?billing=cancel`,
|
|
95
|
+
allow_promotion_codes: true,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return NextResponse.json({
|
|
99
|
+
url: session.url,
|
|
100
|
+
sessionId: session.id,
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error("[Checkout] Error:", error);
|
|
104
|
+
return NextResponse.json(
|
|
105
|
+
{ error: error instanceof Error ? error.message : "Checkout failed" },
|
|
106
|
+
{ status: 500 }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import Stripe from "stripe";
|
|
3
|
+
|
|
4
|
+
const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || "https://adventurous-avocet-799.convex.cloud";
|
|
5
|
+
const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
|
|
6
|
+
|
|
7
|
+
const stripe = stripeSecretKey ? new Stripe(stripeSecretKey) : null;
|
|
8
|
+
|
|
9
|
+
export async function POST(req: NextRequest) {
|
|
10
|
+
if (!stripe) {
|
|
11
|
+
return NextResponse.json({ error: "Stripe not configured" }, { status: 500 });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const { token } = await req.json();
|
|
16
|
+
|
|
17
|
+
if (!token) {
|
|
18
|
+
return NextResponse.json({ error: "Missing token" }, { status: 400 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Verify session and get workspace
|
|
22
|
+
const sessionRes = await fetch(`${CONVEX_URL}/api/query`, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: { "Content-Type": "application/json" },
|
|
25
|
+
body: JSON.stringify({
|
|
26
|
+
path: "workspaces:getSessionWorkspace",
|
|
27
|
+
args: { token },
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const sessionData = await sessionRes.json();
|
|
32
|
+
const workspace = sessionData.value || sessionData;
|
|
33
|
+
|
|
34
|
+
if (!workspace || !workspace._id) {
|
|
35
|
+
return NextResponse.json({ error: "Invalid session" }, { status: 401 });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!workspace.stripeCustomerId) {
|
|
39
|
+
return NextResponse.json({ paymentMethod: null });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Get customer's default payment method
|
|
43
|
+
const customer = await stripe.customers.retrieve(workspace.stripeCustomerId, {
|
|
44
|
+
expand: ["invoice_settings.default_payment_method"],
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (customer.deleted) {
|
|
48
|
+
return NextResponse.json({ paymentMethod: null });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const defaultPm = customer.invoice_settings?.default_payment_method;
|
|
52
|
+
|
|
53
|
+
if (!defaultPm || typeof defaultPm === "string") {
|
|
54
|
+
// Try to get from subscriptions
|
|
55
|
+
const subscriptions = await stripe.subscriptions.list({
|
|
56
|
+
customer: workspace.stripeCustomerId,
|
|
57
|
+
status: "active",
|
|
58
|
+
limit: 1,
|
|
59
|
+
expand: ["data.default_payment_method"],
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (subscriptions.data.length > 0) {
|
|
63
|
+
const subPm = subscriptions.data[0].default_payment_method;
|
|
64
|
+
if (subPm && typeof subPm !== "string" && subPm.type === "card" && subPm.card) {
|
|
65
|
+
return NextResponse.json({
|
|
66
|
+
paymentMethod: {
|
|
67
|
+
brand: subPm.card.brand,
|
|
68
|
+
last4: subPm.card.last4,
|
|
69
|
+
expMonth: subPm.card.exp_month,
|
|
70
|
+
expYear: subPm.card.exp_year,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Try to get any payment method
|
|
77
|
+
const paymentMethods = await stripe.paymentMethods.list({
|
|
78
|
+
customer: workspace.stripeCustomerId,
|
|
79
|
+
type: "card",
|
|
80
|
+
limit: 1,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (paymentMethods.data.length > 0 && paymentMethods.data[0].card) {
|
|
84
|
+
const pm = paymentMethods.data[0];
|
|
85
|
+
return NextResponse.json({
|
|
86
|
+
paymentMethod: {
|
|
87
|
+
brand: pm.card!.brand,
|
|
88
|
+
last4: pm.card!.last4,
|
|
89
|
+
expMonth: pm.card!.exp_month,
|
|
90
|
+
expYear: pm.card!.exp_year,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return NextResponse.json({ paymentMethod: null });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Extract card details from default payment method
|
|
99
|
+
if (defaultPm.type === "card" && defaultPm.card) {
|
|
100
|
+
return NextResponse.json({
|
|
101
|
+
paymentMethod: {
|
|
102
|
+
brand: defaultPm.card.brand,
|
|
103
|
+
last4: defaultPm.card.last4,
|
|
104
|
+
expMonth: defaultPm.card.exp_month,
|
|
105
|
+
expYear: defaultPm.card.exp_year,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return NextResponse.json({ paymentMethod: null });
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error("[Payment Method] Error:", error);
|
|
113
|
+
return NextResponse.json(
|
|
114
|
+
{ error: error instanceof Error ? error.message : "Failed to get payment method" },
|
|
115
|
+
{ status: 500 }
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import Stripe from "stripe";
|
|
3
|
+
|
|
4
|
+
const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || "https://adventurous-avocet-799.convex.cloud";
|
|
5
|
+
const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
|
|
6
|
+
|
|
7
|
+
const stripe = stripeSecretKey ? new Stripe(stripeSecretKey) : null;
|
|
8
|
+
|
|
9
|
+
export async function POST(req: NextRequest) {
|
|
10
|
+
if (!stripe) {
|
|
11
|
+
return NextResponse.json({ error: "Stripe not configured" }, { status: 500 });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const { token } = await req.json();
|
|
16
|
+
|
|
17
|
+
if (!token) {
|
|
18
|
+
return NextResponse.json({ error: "Missing token" }, { status: 400 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Verify session and get workspace
|
|
22
|
+
const sessionRes = await fetch(`${CONVEX_URL}/api/query`, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: { "Content-Type": "application/json" },
|
|
25
|
+
body: JSON.stringify({
|
|
26
|
+
path: "workspaces:getSessionWorkspace",
|
|
27
|
+
args: { token },
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const sessionData = await sessionRes.json();
|
|
32
|
+
const workspace = sessionData.value || sessionData;
|
|
33
|
+
|
|
34
|
+
if (!workspace || !workspace._id) {
|
|
35
|
+
return NextResponse.json({ error: "Invalid session" }, { status: 401 });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!workspace.stripeCustomerId) {
|
|
39
|
+
return NextResponse.json(
|
|
40
|
+
{ error: "No billing account. Add a payment method first." },
|
|
41
|
+
{ status: 400 }
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Determine the app URL for redirects
|
|
46
|
+
const appUrl = process.env.NEXT_PUBLIC_APP_URL || "https://apiclaw.com";
|
|
47
|
+
|
|
48
|
+
// Create Billing Portal session
|
|
49
|
+
const session = await stripe.billingPortal.sessions.create({
|
|
50
|
+
customer: workspace.stripeCustomerId,
|
|
51
|
+
return_url: `${appUrl}/workspace?tab=settings&portal=success`,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return NextResponse.json({
|
|
55
|
+
url: session.url,
|
|
56
|
+
});
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error("[Billing Portal] Error:", error);
|
|
59
|
+
return NextResponse.json(
|
|
60
|
+
{ error: error instanceof Error ? error.message : "Failed to create portal session" },
|
|
61
|
+
{ status: 500 }
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -4,7 +4,7 @@ const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || "https://adventurous-av
|
|
|
4
4
|
|
|
5
5
|
export async function POST(req: NextRequest) {
|
|
6
6
|
try {
|
|
7
|
-
const { email, fingerprint } = await req.json();
|
|
7
|
+
const { email, fingerprint, referralCode } = await req.json();
|
|
8
8
|
|
|
9
9
|
if (!email || !email.includes("@")) {
|
|
10
10
|
return NextResponse.json({ error: "Valid email required" }, { status: 400 });
|
|
@@ -33,8 +33,11 @@ export async function POST(req: NextRequest) {
|
|
|
33
33
|
throw new Error("Failed to get token from Convex");
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
//
|
|
37
|
-
|
|
36
|
+
// Build magic link URL with optional referral code
|
|
37
|
+
let magicLinkUrl = `${process.env.NEXT_PUBLIC_APP_URL || "https://apiclaw.nordsym.com"}/auth/verify?token=${token}`;
|
|
38
|
+
if (referralCode) {
|
|
39
|
+
magicLinkUrl += `&ref=${encodeURIComponent(referralCode)}`;
|
|
40
|
+
}
|
|
38
41
|
|
|
39
42
|
await fetch("https://nordsym.app.n8n.cloud/webhook/symbot-gmail", {
|
|
40
43
|
method: "POST",
|
|
@@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
|
|
|
4
4
|
import { useSearchParams } from "next/navigation";
|
|
5
5
|
import { Suspense } from "react";
|
|
6
6
|
|
|
7
|
-
const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || "https://
|
|
7
|
+
const CONVEX_URL = process.env.NEXT_PUBLIC_CONVEX_URL || "https://adventurous-avocet-799.convex.cloud";
|
|
8
8
|
|
|
9
9
|
interface VerifyResult {
|
|
10
10
|
success: boolean;
|
|
@@ -23,6 +23,7 @@ interface VerifyResult {
|
|
|
23
23
|
function VerifyContent() {
|
|
24
24
|
const searchParams = useSearchParams();
|
|
25
25
|
const token = searchParams.get("token");
|
|
26
|
+
const referralCode = searchParams.get("ref"); // Referral code from signup URL
|
|
26
27
|
|
|
27
28
|
const [status, setStatus] = useState<"loading" | "success" | "error">("loading");
|
|
28
29
|
const [error, setError] = useState<string>("");
|
|
@@ -43,17 +44,23 @@ function VerifyContent() {
|
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
verifyToken(token);
|
|
47
|
-
}, [token]);
|
|
47
|
+
verifyToken(token, referralCode || undefined);
|
|
48
|
+
}, [token, referralCode]);
|
|
48
49
|
|
|
49
|
-
async function verifyToken(token: string) {
|
|
50
|
+
async function verifyToken(token: string, refCode?: string) {
|
|
50
51
|
try {
|
|
52
|
+
// Build args with optional referral code
|
|
53
|
+
const args: { token: string; referralCode?: string } = { token };
|
|
54
|
+
if (refCode) {
|
|
55
|
+
args.referralCode = refCode;
|
|
56
|
+
}
|
|
57
|
+
|
|
51
58
|
const response = await fetch(`${CONVEX_URL}/api/mutation`, {
|
|
52
59
|
method: "POST",
|
|
53
60
|
headers: { "Content-Type": "application/json" },
|
|
54
61
|
body: JSON.stringify({
|
|
55
62
|
path: "workspaces:verifyMagicLink",
|
|
56
|
-
args
|
|
63
|
+
args,
|
|
57
64
|
}),
|
|
58
65
|
});
|
|
59
66
|
|
|
@@ -68,6 +75,15 @@ function VerifyContent() {
|
|
|
68
75
|
setStatus("success");
|
|
69
76
|
setWorkspace(data.workspace || null);
|
|
70
77
|
setSessionToken(data.sessionToken || "");
|
|
78
|
+
|
|
79
|
+
// Store session for workspace dashboard
|
|
80
|
+
if (data.sessionToken) {
|
|
81
|
+
localStorage.setItem("apiclaw_workspace_session", data.sessionToken);
|
|
82
|
+
// Auto-redirect to workspace after 2 seconds
|
|
83
|
+
setTimeout(() => {
|
|
84
|
+
window.location.href = "/workspace";
|
|
85
|
+
}, 2000);
|
|
86
|
+
}
|
|
71
87
|
} else {
|
|
72
88
|
setStatus("error");
|
|
73
89
|
switch (data.error) {
|
|
@@ -235,9 +251,15 @@ function VerifyContent() {
|
|
|
235
251
|
</div>
|
|
236
252
|
)}
|
|
237
253
|
|
|
238
|
-
<div className="text-center pt-4 border-t border-[var(--border)]">
|
|
254
|
+
<div className="text-center pt-4 border-t border-[var(--border)] space-y-3">
|
|
255
|
+
<a
|
|
256
|
+
href="/workspace"
|
|
257
|
+
className="inline-flex items-center justify-center px-6 py-3 bg-[var(--accent)] text-white rounded-lg font-medium hover:opacity-90 transition w-full"
|
|
258
|
+
>
|
|
259
|
+
Go to Workspace →
|
|
260
|
+
</a>
|
|
239
261
|
<p className="text-[var(--text-muted)] text-sm">
|
|
240
|
-
✓
|
|
262
|
+
✓ Or close this tab if using CLI
|
|
241
263
|
</p>
|
|
242
264
|
</div>
|
|
243
265
|
</div>
|
|
@@ -267,8 +289,8 @@ function VerifyContent() {
|
|
|
267
289
|
{/* Footer */}
|
|
268
290
|
<p className="text-center text-[var(--text-muted)] text-xs mt-6">
|
|
269
291
|
Having trouble? Contact{" "}
|
|
270
|
-
<a href="mailto:
|
|
271
|
-
|
|
292
|
+
<a href="mailto:support_apiclaw@nordsym.com" className="text-[var(--accent)] hover:underline">
|
|
293
|
+
support_apiclaw@nordsym.com
|
|
272
294
|
</a>
|
|
273
295
|
</p>
|
|
274
296
|
</div>
|
|
@@ -311,7 +311,7 @@ export default function DocsPage() {
|
|
|
311
311
|
</li>
|
|
312
312
|
<li>
|
|
313
313
|
<strong>Email:</strong>{' '}
|
|
314
|
-
<a href="mailto:
|
|
314
|
+
<a href="mailto:support_apiclaw@nordsym.com" className="text-[var(--accent)] hover:underline">support_apiclaw@nordsym.com</a>
|
|
315
315
|
</li>
|
|
316
316
|
</ul>
|
|
317
317
|
</div>
|
|
@@ -10,7 +10,7 @@ const EARN_CHANNELS = [
|
|
|
10
10
|
iconName: 'star',
|
|
11
11
|
title: 'Star on GitHub',
|
|
12
12
|
description: 'Show some love on GitHub',
|
|
13
|
-
credits:
|
|
13
|
+
credits: 20,
|
|
14
14
|
cta: 'Star Repository',
|
|
15
15
|
href: 'https://github.com/nordsym/apiclaw',
|
|
16
16
|
color: 'from-red-500/20 to-orange-500/10',
|
|
@@ -22,7 +22,7 @@ const EARN_CHANNELS = [
|
|
|
22
22
|
iconName: 'twitter',
|
|
23
23
|
title: 'Follow @NordSym',
|
|
24
24
|
description: 'Stay updated on X/Twitter',
|
|
25
|
-
credits:
|
|
25
|
+
credits: 15,
|
|
26
26
|
cta: 'Follow Us',
|
|
27
27
|
href: 'https://x.com/NordSym',
|
|
28
28
|
color: 'from-red-400/20 to-rose-500/10',
|
|
@@ -34,7 +34,7 @@ const EARN_CHANNELS = [
|
|
|
34
34
|
iconName: 'mail',
|
|
35
35
|
title: 'Join Newsletter',
|
|
36
36
|
description: 'Get weekly updates & tips',
|
|
37
|
-
credits:
|
|
37
|
+
credits: 15,
|
|
38
38
|
cta: 'Subscribe',
|
|
39
39
|
href: '#newsletter',
|
|
40
40
|
color: 'from-rose-500/20 to-red-500/10',
|
|
@@ -45,8 +45,8 @@ const EARN_CHANNELS = [
|
|
|
45
45
|
id: 'referral',
|
|
46
46
|
iconName: 'users',
|
|
47
47
|
title: 'Invite Friends',
|
|
48
|
-
description: 'Earn for each friend
|
|
49
|
-
credits:
|
|
48
|
+
description: 'Earn 10 calls for each friend',
|
|
49
|
+
credits: 10,
|
|
50
50
|
perUnit: 'per friend',
|
|
51
51
|
cta: 'Copy Link',
|
|
52
52
|
color: 'from-orange-500/20 to-red-500/10',
|
|
@@ -72,7 +72,7 @@ export default function EarnPage() {
|
|
|
72
72
|
const [copied, setCopied] = useState(false);
|
|
73
73
|
const [email, setEmail] = useState('');
|
|
74
74
|
const [subscribed, setSubscribed] = useState(false);
|
|
75
|
-
const [totalCredits] = useState(
|
|
75
|
+
const [totalCredits] = useState(50); // Demo value - matches free tier max earn
|
|
76
76
|
const [theme, setTheme] = useState<'light' | 'dark'>('light');
|
|
77
77
|
|
|
78
78
|
useEffect(() => {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { useSearchParams, useRouter } from "next/navigation";
|
|
5
|
+
import { Suspense } from "react";
|
|
6
|
+
|
|
7
|
+
function JoinContent() {
|
|
8
|
+
const searchParams = useSearchParams();
|
|
9
|
+
const router = useRouter();
|
|
10
|
+
const refCode = searchParams.get("ref");
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
// Store referral code in localStorage for use during signup
|
|
14
|
+
if (refCode) {
|
|
15
|
+
localStorage.setItem("apiclaw_referral_code", refCode);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Redirect to login page
|
|
19
|
+
router.push("/login");
|
|
20
|
+
}, [refCode, router]);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="min-h-screen bg-[var(--background)] flex items-center justify-center p-4">
|
|
24
|
+
<div className="text-center">
|
|
25
|
+
<span className="text-6xl animate-bounce">🦞</span>
|
|
26
|
+
<p className="mt-4 text-[var(--text-secondary)]">
|
|
27
|
+
{refCode ? "Processing your invite..." : "Redirecting to login..."}
|
|
28
|
+
</p>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default function JoinPage() {
|
|
35
|
+
return (
|
|
36
|
+
<Suspense
|
|
37
|
+
fallback={
|
|
38
|
+
<div className="min-h-screen bg-[var(--background)] flex items-center justify-center">
|
|
39
|
+
<div className="text-center">
|
|
40
|
+
<span className="text-6xl">🦞</span>
|
|
41
|
+
<p className="mt-4 text-[var(--text-secondary)]">Loading...</p>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
}
|
|
45
|
+
>
|
|
46
|
+
<JoinContent />
|
|
47
|
+
</Suspense>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -48,10 +48,13 @@ export default function LoginPage() {
|
|
|
48
48
|
setError(null);
|
|
49
49
|
|
|
50
50
|
try {
|
|
51
|
+
// Check for stored referral code (from /join page)
|
|
52
|
+
const referralCode = localStorage.getItem("apiclaw_referral_code");
|
|
53
|
+
|
|
51
54
|
const response = await fetch("/api/workspace-auth/magic-link", {
|
|
52
55
|
method: "POST",
|
|
53
56
|
headers: { "Content-Type": "application/json" },
|
|
54
|
-
body: JSON.stringify({ email }),
|
|
57
|
+
body: JSON.stringify({ email, referralCode }),
|
|
55
58
|
});
|
|
56
59
|
|
|
57
60
|
if (!response.ok) {
|
|
@@ -59,6 +62,9 @@ export default function LoginPage() {
|
|
|
59
62
|
throw new Error(data.error || "Failed to send magic link");
|
|
60
63
|
}
|
|
61
64
|
|
|
65
|
+
// Clear referral code after sending (it will be in the email link)
|
|
66
|
+
localStorage.removeItem("apiclaw_referral_code");
|
|
67
|
+
|
|
62
68
|
setIsSent(true);
|
|
63
69
|
} catch (err) {
|
|
64
70
|
setError(err instanceof Error ? err.message : "Something went wrong");
|
|
@@ -175,7 +181,7 @@ export default function LoginPage() {
|
|
|
175
181
|
<div className="space-y-3 text-sm text-[var(--text-muted)]">
|
|
176
182
|
<div className="flex items-center gap-2">
|
|
177
183
|
<Check className="w-4 h-4 text-green-500" />
|
|
178
|
-
<span>
|
|
184
|
+
<span>50 free API calls per month</span>
|
|
179
185
|
</div>
|
|
180
186
|
<div className="flex items-center gap-2">
|
|
181
187
|
<Check className="w-4 h-4 text-green-500" />
|