@mars-stack/core 0.4.0
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 +32 -0
- package/cursor/manifest.json +304 -0
- package/cursor/rules/mars-composition-patterns.mdc +186 -0
- package/cursor/rules/mars-data-access.mdc +26 -0
- package/cursor/rules/mars-project-structure.mdc +34 -0
- package/cursor/rules/mars-security.mdc +25 -0
- package/cursor/rules/mars-testing.mdc +24 -0
- package/cursor/rules/mars-ui-conventions.mdc +29 -0
- package/cursor/skills/mars-add-api-route/SKILL.md +120 -0
- package/cursor/skills/mars-add-audit-log/SKILL.md +373 -0
- package/cursor/skills/mars-add-blog/SKILL.md +447 -0
- package/cursor/skills/mars-add-command-palette/SKILL.md +438 -0
- package/cursor/skills/mars-add-component/SKILL.md +158 -0
- package/cursor/skills/mars-add-crud-routes/SKILL.md +221 -0
- package/cursor/skills/mars-add-e2e-test/SKILL.md +227 -0
- package/cursor/skills/mars-add-error-boundary/SKILL.md +472 -0
- package/cursor/skills/mars-add-feature/SKILL.md +174 -0
- package/cursor/skills/mars-add-middleware/SKILL.md +135 -0
- package/cursor/skills/mars-add-page/SKILL.md +153 -0
- package/cursor/skills/mars-add-prisma-model/SKILL.md +148 -0
- package/cursor/skills/mars-add-protected-resource/SKILL.md +192 -0
- package/cursor/skills/mars-add-role/SKILL.md +156 -0
- package/cursor/skills/mars-add-server-action/SKILL.md +167 -0
- package/cursor/skills/mars-add-webhook/SKILL.md +192 -0
- package/cursor/skills/mars-build-complete-feature/SKILL.md +228 -0
- package/cursor/skills/mars-build-dashboard/SKILL.md +211 -0
- package/cursor/skills/mars-build-data-table/SKILL.md +284 -0
- package/cursor/skills/mars-build-form/SKILL.md +229 -0
- package/cursor/skills/mars-build-landing-page/SKILL.md +248 -0
- package/cursor/skills/mars-capture-conversation-context/SKILL.md +119 -0
- package/cursor/skills/mars-configure-ai/SKILL.md +617 -0
- package/cursor/skills/mars-configure-analytics/SKILL.md +413 -0
- package/cursor/skills/mars-configure-dark-mode/SKILL.md +309 -0
- package/cursor/skills/mars-configure-email/SKILL.md +170 -0
- package/cursor/skills/mars-configure-email-verification/SKILL.md +333 -0
- package/cursor/skills/mars-configure-feature-flags/SKILL.md +361 -0
- package/cursor/skills/mars-configure-i18n/SKILL.md +518 -0
- package/cursor/skills/mars-configure-jobs/SKILL.md +500 -0
- package/cursor/skills/mars-configure-magic-links/SKILL.md +385 -0
- package/cursor/skills/mars-configure-multi-tenancy/SKILL.md +611 -0
- package/cursor/skills/mars-configure-notifications/SKILL.md +569 -0
- package/cursor/skills/mars-configure-oauth/SKILL.md +217 -0
- package/cursor/skills/mars-configure-onboarding/SKILL.md +483 -0
- package/cursor/skills/mars-configure-payments/SKILL.md +243 -0
- package/cursor/skills/mars-configure-realtime/SKILL.md +733 -0
- package/cursor/skills/mars-configure-search/SKILL.md +581 -0
- package/cursor/skills/mars-configure-storage/SKILL.md +273 -0
- package/cursor/skills/mars-configure-two-factor/SKILL.md +518 -0
- package/cursor/skills/mars-create-execution-plan/SKILL.md +204 -0
- package/cursor/skills/mars-create-seed/SKILL.md +191 -0
- package/cursor/skills/mars-deploy-to-vercel/SKILL.md +300 -0
- package/cursor/skills/mars-design-tokens/SKILL.md +138 -0
- package/cursor/skills/mars-setup-billing/SKILL.md +322 -0
- package/cursor/skills/mars-setup-project/SKILL.md +104 -0
- package/cursor/skills/mars-setup-teams/SKILL.md +688 -0
- package/cursor/skills/mars-test-api-route/SKILL.md +219 -0
- package/cursor/skills/mars-update-architecture-docs/SKILL.md +189 -0
- package/dist/api-error/index.d.ts +27 -0
- package/dist/api-error/index.d.ts.map +1 -0
- package/dist/api-error/index.js +2 -0
- package/dist/auth/credential-tag.d.ts +5 -0
- package/dist/auth/credential-tag.d.ts.map +1 -0
- package/dist/auth/credential-tag.js +2 -0
- package/dist/auth/crypto-utils.d.ts +43 -0
- package/dist/auth/crypto-utils.d.ts.map +1 -0
- package/dist/auth/crypto-utils.js +1 -0
- package/dist/auth/csrf.d.ts +32 -0
- package/dist/auth/csrf.d.ts.map +1 -0
- package/dist/auth/csrf.js +2 -0
- package/dist/auth/hooks/index.d.ts +4 -0
- package/dist/auth/hooks/index.d.ts.map +1 -0
- package/dist/auth/hooks/index.js +68 -0
- package/dist/auth/hooks/useCSRF.d.ts +7 -0
- package/dist/auth/hooks/useCSRF.d.ts.map +1 -0
- package/dist/auth/hooks/usePasswordStrength.d.ts +17 -0
- package/dist/auth/hooks/usePasswordStrength.d.ts.map +1 -0
- package/dist/auth/internal-api-key.d.ts +5 -0
- package/dist/auth/internal-api-key.d.ts.map +1 -0
- package/dist/auth/internal-api-key.js +30 -0
- package/dist/auth/link-utils.d.ts +13 -0
- package/dist/auth/link-utils.d.ts.map +1 -0
- package/dist/auth/link-utils.js +1 -0
- package/dist/auth/middleware.d.ts +56 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +3 -0
- package/dist/auth/password.d.ts +28 -0
- package/dist/auth/password.d.ts.map +1 -0
- package/dist/auth/password.js +1 -0
- package/dist/auth/reset-token.d.ts +3 -0
- package/dist/auth/reset-token.d.ts.map +1 -0
- package/dist/auth/reset-token.js +9 -0
- package/dist/auth/responses.d.ts +15 -0
- package/dist/auth/responses.d.ts.map +1 -0
- package/dist/auth/responses.js +2 -0
- package/dist/auth/session.d.ts +79 -0
- package/dist/auth/session.d.ts.map +1 -0
- package/dist/auth/session.js +1 -0
- package/dist/auth/types.d.ts +18 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +10 -0
- package/dist/auth/validation.d.ts +146 -0
- package/dist/auth/validation.d.ts.map +1 -0
- package/dist/auth/validation.js +116 -0
- package/dist/auth/validators.d.ts +4 -0
- package/dist/auth/validators.d.ts.map +1 -0
- package/dist/auth/validators.js +27 -0
- package/dist/auth/verification.d.ts +54 -0
- package/dist/auth/verification.d.ts.map +1 -0
- package/dist/auth/verification.js +39 -0
- package/dist/chunk-4LS3QDD5.js +162 -0
- package/dist/chunk-ABBUHT5Z.js +110 -0
- package/dist/chunk-CTYAVMOF.js +15 -0
- package/dist/chunk-GVLH2GQP.js +14 -0
- package/dist/chunk-HOSMMQMA.js +109 -0
- package/dist/chunk-MXQ66RUN.js +28 -0
- package/dist/chunk-PZE3JGXO.js +149 -0
- package/dist/chunk-QAH2Y5WK.js +93 -0
- package/dist/chunk-QWMN5UJC.js +76 -0
- package/dist/chunk-ROQV54MU.js +117 -0
- package/dist/chunk-U4NZQ366.js +46 -0
- package/dist/chunk-WBJOIENS.js +22 -0
- package/dist/chunk-WO6FHJHG.js +29 -0
- package/dist/chunk-Z5BEKPJI.js +96 -0
- package/dist/chunk-ZA46T6GX.js +24 -0
- package/dist/configure-mars.d.ts +104 -0
- package/dist/configure-mars.d.ts.map +1 -0
- package/dist/database/index.d.ts +8 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +1 -0
- package/dist/email/index.d.ts +25 -0
- package/dist/email/index.d.ts.map +1 -0
- package/dist/email/index.js +2 -0
- package/dist/email/types.d.ts +18 -0
- package/dist/email/types.d.ts.map +1 -0
- package/dist/env/index.d.ts +36 -0
- package/dist/env/index.d.ts.map +1 -0
- package/dist/env/index.js +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +163 -0
- package/dist/logger/index.d.ts +80 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/logger/index.js +1 -0
- package/dist/payments/index.d.ts +53 -0
- package/dist/payments/index.d.ts.map +1 -0
- package/dist/payments/index.js +72 -0
- package/dist/plugin/builtin/email-plugins.d.ts +10 -0
- package/dist/plugin/builtin/email-plugins.d.ts.map +1 -0
- package/dist/plugin/builtin/index.d.ts +4 -0
- package/dist/plugin/builtin/index.d.ts.map +1 -0
- package/dist/plugin/builtin/index.js +324 -0
- package/dist/plugin/builtin/payment-plugins.d.ts +4 -0
- package/dist/plugin/builtin/payment-plugins.d.ts.map +1 -0
- package/dist/plugin/builtin/storage-plugins.d.ts +5 -0
- package/dist/plugin/builtin/storage-plugins.d.ts.map +1 -0
- package/dist/plugin/index.d.ts +21 -0
- package/dist/plugin/index.d.ts.map +1 -0
- package/dist/plugin/index.js +30 -0
- package/dist/rate-limit/index.d.ts +89 -0
- package/dist/rate-limit/index.d.ts.map +1 -0
- package/dist/rate-limit/index.js +166 -0
- package/dist/seo/faq.d.ts +37 -0
- package/dist/seo/faq.d.ts.map +1 -0
- package/dist/seo/index.d.ts +75 -0
- package/dist/seo/index.d.ts.map +1 -0
- package/dist/seo/index.js +1 -0
- package/dist/storage/index.d.ts +50 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +211 -0
- package/dist/test-utils/factories.d.ts +38 -0
- package/dist/test-utils/factories.d.ts.map +1 -0
- package/dist/test-utils/index.d.ts +6 -0
- package/dist/test-utils/index.d.ts.map +1 -0
- package/dist/test-utils/index.js +117 -0
- package/dist/test-utils/mock-auth.d.ts +25 -0
- package/dist/test-utils/mock-auth.d.ts.map +1 -0
- package/dist/test-utils/mock-prisma.d.ts +55 -0
- package/dist/test-utils/mock-prisma.d.ts.map +1 -0
- package/dist/test-utils/render.d.ts +4 -0
- package/dist/test-utils/render.d.ts.map +1 -0
- package/dist/test-utils/request-helpers.d.ts +6 -0
- package/dist/test-utils/request-helpers.d.ts.map +1 -0
- package/dist/types.d.ts +53 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/math.d.ts +2 -0
- package/dist/utils/math.d.ts.map +1 -0
- package/dist/utils/math.js +7 -0
- package/dist/utils/optional-import.d.ts +14 -0
- package/dist/utils/optional-import.d.ts.map +1 -0
- package/package.json +205 -0
- package/scripts/generate-skill-adapters.ts +146 -0
- package/scripts/postinstall.mjs +146 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { importOptional } from '../../chunk-CTYAVMOF.js';
|
|
2
|
+
|
|
3
|
+
// src/plugin/builtin/email-plugins.ts
|
|
4
|
+
function createSendGridPlugin() {
|
|
5
|
+
let appName = "";
|
|
6
|
+
return {
|
|
7
|
+
name: "sendgrid",
|
|
8
|
+
version: "1.0.0",
|
|
9
|
+
type: "email",
|
|
10
|
+
configure(config) {
|
|
11
|
+
appName = config.appName;
|
|
12
|
+
},
|
|
13
|
+
validate() {
|
|
14
|
+
const errors = [];
|
|
15
|
+
const warnings = [];
|
|
16
|
+
if (!process.env.SENDGRID_API_KEY) {
|
|
17
|
+
errors.push("SENDGRID_API_KEY environment variable is not set");
|
|
18
|
+
}
|
|
19
|
+
if (!process.env.SENDGRID_FROM_EMAIL) {
|
|
20
|
+
errors.push("SENDGRID_FROM_EMAIL environment variable is not set");
|
|
21
|
+
}
|
|
22
|
+
if (!appName) {
|
|
23
|
+
warnings.push("Plugin not configured \u2014 call configure() with appName");
|
|
24
|
+
}
|
|
25
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
26
|
+
},
|
|
27
|
+
async send(params) {
|
|
28
|
+
const sgMail = await importOptional("@sendgrid/mail");
|
|
29
|
+
const apiKey = process.env.SENDGRID_API_KEY;
|
|
30
|
+
const fromEmail = process.env.SENDGRID_FROM_EMAIL;
|
|
31
|
+
if (!apiKey) throw new Error("SENDGRID_API_KEY is not set");
|
|
32
|
+
if (!fromEmail) throw new Error("SENDGRID_FROM_EMAIL is not set");
|
|
33
|
+
sgMail.default.setApiKey(apiKey);
|
|
34
|
+
await sgMail.default.send({
|
|
35
|
+
to: params.to,
|
|
36
|
+
from: { email: fromEmail, name: appName },
|
|
37
|
+
subject: params.subject,
|
|
38
|
+
text: params.text ?? params.html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim(),
|
|
39
|
+
html: params.html,
|
|
40
|
+
mailSettings: {
|
|
41
|
+
bypassListManagement: { enable: true },
|
|
42
|
+
sandboxMode: { enable: false }
|
|
43
|
+
},
|
|
44
|
+
trackingSettings: {
|
|
45
|
+
clickTracking: { enable: false },
|
|
46
|
+
openTracking: { enable: false },
|
|
47
|
+
subscriptionTracking: { enable: false }
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function createResendPlugin() {
|
|
54
|
+
let appName = "";
|
|
55
|
+
return {
|
|
56
|
+
name: "resend",
|
|
57
|
+
version: "1.0.0",
|
|
58
|
+
type: "email",
|
|
59
|
+
configure(config) {
|
|
60
|
+
appName = config.appName;
|
|
61
|
+
},
|
|
62
|
+
validate() {
|
|
63
|
+
const errors = [];
|
|
64
|
+
const warnings = [];
|
|
65
|
+
if (!process.env.RESEND_API_KEY) {
|
|
66
|
+
errors.push("RESEND_API_KEY environment variable is not set");
|
|
67
|
+
}
|
|
68
|
+
if (!process.env.RESEND_FROM_EMAIL) {
|
|
69
|
+
errors.push("RESEND_FROM_EMAIL environment variable is not set");
|
|
70
|
+
}
|
|
71
|
+
if (!appName) {
|
|
72
|
+
warnings.push("Plugin not configured \u2014 call configure() with appName");
|
|
73
|
+
}
|
|
74
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
75
|
+
},
|
|
76
|
+
async send(params) {
|
|
77
|
+
const { Resend } = await importOptional("resend");
|
|
78
|
+
const apiKey = process.env.RESEND_API_KEY;
|
|
79
|
+
const fromEmail = process.env.RESEND_FROM_EMAIL;
|
|
80
|
+
if (!apiKey) throw new Error("RESEND_API_KEY is not set");
|
|
81
|
+
if (!fromEmail) throw new Error("RESEND_FROM_EMAIL is not set");
|
|
82
|
+
const resend = new Resend(apiKey);
|
|
83
|
+
await resend.emails.send({
|
|
84
|
+
from: `${appName} <${fromEmail}>`,
|
|
85
|
+
to: params.to,
|
|
86
|
+
subject: params.subject,
|
|
87
|
+
html: params.html,
|
|
88
|
+
text: params.text
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function createConsoleEmailPlugin() {
|
|
94
|
+
return {
|
|
95
|
+
name: "console",
|
|
96
|
+
version: "1.0.0",
|
|
97
|
+
type: "email",
|
|
98
|
+
configure() {
|
|
99
|
+
},
|
|
100
|
+
validate() {
|
|
101
|
+
return { valid: true, errors: [], warnings: ["Console email provider is for development only"] };
|
|
102
|
+
},
|
|
103
|
+
async send(params) {
|
|
104
|
+
const { formatConsoleEmail } = await import('../../email/index.js');
|
|
105
|
+
console.log(formatConsoleEmail(params));
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/plugin/builtin/payment-plugins.ts
|
|
111
|
+
function createStripePlugin() {
|
|
112
|
+
return {
|
|
113
|
+
name: "stripe",
|
|
114
|
+
version: "1.0.0",
|
|
115
|
+
type: "payment",
|
|
116
|
+
configure() {
|
|
117
|
+
},
|
|
118
|
+
validate() {
|
|
119
|
+
const errors = [];
|
|
120
|
+
const warnings = [];
|
|
121
|
+
if (!process.env.STRIPE_SECRET_KEY) {
|
|
122
|
+
errors.push("STRIPE_SECRET_KEY environment variable is not set");
|
|
123
|
+
}
|
|
124
|
+
if (!process.env.STRIPE_WEBHOOK_SECRET) {
|
|
125
|
+
warnings.push("STRIPE_WEBHOOK_SECRET is not set \u2014 webhooks will fail");
|
|
126
|
+
}
|
|
127
|
+
if (!process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY) {
|
|
128
|
+
warnings.push("NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is not set \u2014 client-side Stripe will fail");
|
|
129
|
+
}
|
|
130
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
131
|
+
},
|
|
132
|
+
async createCheckoutSession(params) {
|
|
133
|
+
const Stripe = (await importOptional("stripe")).default;
|
|
134
|
+
const secretKey = process.env.STRIPE_SECRET_KEY;
|
|
135
|
+
if (!secretKey) throw new Error("STRIPE_SECRET_KEY is not set");
|
|
136
|
+
const stripe = new Stripe(secretKey);
|
|
137
|
+
const session = await stripe.checkout.sessions.create({
|
|
138
|
+
customer: params.customerId,
|
|
139
|
+
mode: "subscription",
|
|
140
|
+
line_items: [{ price: params.priceId, quantity: 1 }],
|
|
141
|
+
success_url: params.successUrl,
|
|
142
|
+
cancel_url: params.cancelUrl,
|
|
143
|
+
metadata: params.metadata
|
|
144
|
+
});
|
|
145
|
+
if (!session.url) {
|
|
146
|
+
throw new Error("Stripe checkout session created without a URL");
|
|
147
|
+
}
|
|
148
|
+
return { url: session.url };
|
|
149
|
+
},
|
|
150
|
+
async createPortalSession(params) {
|
|
151
|
+
const Stripe = (await importOptional("stripe")).default;
|
|
152
|
+
const secretKey = process.env.STRIPE_SECRET_KEY;
|
|
153
|
+
if (!secretKey) throw new Error("STRIPE_SECRET_KEY is not set");
|
|
154
|
+
const stripe = new Stripe(secretKey);
|
|
155
|
+
const session = await stripe.billingPortal.sessions.create({
|
|
156
|
+
customer: params.customerId,
|
|
157
|
+
return_url: params.returnUrl
|
|
158
|
+
});
|
|
159
|
+
return { url: session.url };
|
|
160
|
+
},
|
|
161
|
+
async constructWebhookEvent(body, signature) {
|
|
162
|
+
const Stripe = (await importOptional("stripe")).default;
|
|
163
|
+
const secretKey = process.env.STRIPE_SECRET_KEY;
|
|
164
|
+
if (!secretKey) throw new Error("STRIPE_SECRET_KEY is not set");
|
|
165
|
+
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
|
|
166
|
+
if (!webhookSecret) throw new Error("STRIPE_WEBHOOK_SECRET is not set");
|
|
167
|
+
const stripe = new Stripe(secretKey);
|
|
168
|
+
return stripe.webhooks.constructEvent(body, signature, webhookSecret);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/plugin/builtin/storage-plugins.ts
|
|
174
|
+
function createVercelBlobPlugin() {
|
|
175
|
+
return {
|
|
176
|
+
name: "vercel-blob",
|
|
177
|
+
version: "1.0.0",
|
|
178
|
+
type: "storage",
|
|
179
|
+
configure() {
|
|
180
|
+
},
|
|
181
|
+
validate() {
|
|
182
|
+
const errors = [];
|
|
183
|
+
if (!process.env.BLOB_READ_WRITE_TOKEN) {
|
|
184
|
+
errors.push("BLOB_READ_WRITE_TOKEN environment variable is not set");
|
|
185
|
+
}
|
|
186
|
+
return { valid: errors.length === 0, errors, warnings: [] };
|
|
187
|
+
},
|
|
188
|
+
async upload(params) {
|
|
189
|
+
const { put } = await importOptional("@vercel/blob");
|
|
190
|
+
const safe = sanitizeFilename(params.filename);
|
|
191
|
+
const blob = await put(safe, params.data, {
|
|
192
|
+
access: "public",
|
|
193
|
+
contentType: params.contentType,
|
|
194
|
+
addRandomSuffix: true
|
|
195
|
+
});
|
|
196
|
+
return {
|
|
197
|
+
url: blob.url,
|
|
198
|
+
pathname: blob.pathname,
|
|
199
|
+
contentType: params.contentType,
|
|
200
|
+
size: getDataSize(params.data) ?? 0
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
async delete(url) {
|
|
204
|
+
const { del } = await importOptional("@vercel/blob");
|
|
205
|
+
await del(url);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function createS3Plugin() {
|
|
210
|
+
return {
|
|
211
|
+
name: "s3",
|
|
212
|
+
version: "1.0.0",
|
|
213
|
+
type: "storage",
|
|
214
|
+
configure() {
|
|
215
|
+
},
|
|
216
|
+
validate() {
|
|
217
|
+
const errors = [];
|
|
218
|
+
if (!process.env.AWS_REGION) errors.push("AWS_REGION environment variable is not set");
|
|
219
|
+
if (!process.env.S3_BUCKET_NAME) errors.push("S3_BUCKET_NAME environment variable is not set");
|
|
220
|
+
if (!process.env.AWS_ACCESS_KEY_ID) errors.push("AWS_ACCESS_KEY_ID environment variable is not set");
|
|
221
|
+
if (!process.env.AWS_SECRET_ACCESS_KEY) errors.push("AWS_SECRET_ACCESS_KEY environment variable is not set");
|
|
222
|
+
return { valid: errors.length === 0, errors, warnings: [] };
|
|
223
|
+
},
|
|
224
|
+
async upload(params) {
|
|
225
|
+
const { PutObjectCommand, S3Client } = await importOptional("@aws-sdk/client-s3");
|
|
226
|
+
const config = getS3Config();
|
|
227
|
+
const client = new S3Client({
|
|
228
|
+
region: config.region,
|
|
229
|
+
credentials: {
|
|
230
|
+
accessKeyId: config.accessKeyId,
|
|
231
|
+
secretAccessKey: config.secretAccessKey
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
const safe = sanitizeFilename(params.filename);
|
|
235
|
+
const key = `uploads/${Date.now()}-${safe}`;
|
|
236
|
+
let body;
|
|
237
|
+
if (Buffer.isBuffer(params.data)) {
|
|
238
|
+
body = params.data;
|
|
239
|
+
} else if (params.data instanceof Blob) {
|
|
240
|
+
body = params.data;
|
|
241
|
+
} else {
|
|
242
|
+
const chunks = [];
|
|
243
|
+
const reader = params.data.getReader();
|
|
244
|
+
let done = false;
|
|
245
|
+
while (!done) {
|
|
246
|
+
const result = await reader.read();
|
|
247
|
+
done = result.done;
|
|
248
|
+
if (result.value) chunks.push(result.value);
|
|
249
|
+
}
|
|
250
|
+
body = Buffer.concat(chunks);
|
|
251
|
+
}
|
|
252
|
+
await client.send(
|
|
253
|
+
new PutObjectCommand({
|
|
254
|
+
Bucket: config.bucket,
|
|
255
|
+
Key: key,
|
|
256
|
+
Body: body,
|
|
257
|
+
ContentType: params.contentType,
|
|
258
|
+
ACL: params.access === "public" ? "public-read" : "private"
|
|
259
|
+
})
|
|
260
|
+
);
|
|
261
|
+
const url = `https://${config.bucket}.s3.${config.region}.amazonaws.com/${key}`;
|
|
262
|
+
return {
|
|
263
|
+
url,
|
|
264
|
+
pathname: key,
|
|
265
|
+
contentType: params.contentType,
|
|
266
|
+
size: Buffer.isBuffer(body) ? body.byteLength : body.size
|
|
267
|
+
};
|
|
268
|
+
},
|
|
269
|
+
async delete(url) {
|
|
270
|
+
const { DeleteObjectCommand, S3Client } = await importOptional("@aws-sdk/client-s3");
|
|
271
|
+
const config = getS3Config();
|
|
272
|
+
const client = new S3Client({
|
|
273
|
+
region: config.region,
|
|
274
|
+
credentials: {
|
|
275
|
+
accessKeyId: config.accessKeyId,
|
|
276
|
+
secretAccessKey: config.secretAccessKey
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
const urlObj = new URL(url);
|
|
280
|
+
const key = urlObj.pathname.slice(1);
|
|
281
|
+
await client.send(
|
|
282
|
+
new DeleteObjectCommand({ Bucket: config.bucket, Key: key })
|
|
283
|
+
);
|
|
284
|
+
},
|
|
285
|
+
async getSignedUrl(url, expiresIn = 3600) {
|
|
286
|
+
const { GetObjectCommand, S3Client } = await importOptional("@aws-sdk/client-s3");
|
|
287
|
+
const { getSignedUrl: s3GetSignedUrl } = await importOptional("@aws-sdk/s3-request-presigner");
|
|
288
|
+
const config = getS3Config();
|
|
289
|
+
const client = new S3Client({
|
|
290
|
+
region: config.region,
|
|
291
|
+
credentials: {
|
|
292
|
+
accessKeyId: config.accessKeyId,
|
|
293
|
+
secretAccessKey: config.secretAccessKey
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
const urlObj = new URL(url);
|
|
297
|
+
const key = urlObj.pathname.slice(1);
|
|
298
|
+
return s3GetSignedUrl(client, new GetObjectCommand({ Bucket: config.bucket, Key: key }), {
|
|
299
|
+
expiresIn
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function sanitizeFilename(filename) {
|
|
305
|
+
return filename.replace(/[^\w.\-]/g, "_").replace(/\.{2,}/g, ".").slice(0, 255);
|
|
306
|
+
}
|
|
307
|
+
function getDataSize(data) {
|
|
308
|
+
if (Buffer.isBuffer(data)) return data.byteLength;
|
|
309
|
+
if (data instanceof Blob) return data.size;
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
function getS3Config() {
|
|
313
|
+
const region = process.env.AWS_REGION;
|
|
314
|
+
const bucket = process.env.S3_BUCKET_NAME;
|
|
315
|
+
const accessKeyId = process.env.AWS_ACCESS_KEY_ID;
|
|
316
|
+
const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
|
|
317
|
+
if (!region) throw new Error("AWS_REGION is not set");
|
|
318
|
+
if (!bucket) throw new Error("S3_BUCKET_NAME is not set");
|
|
319
|
+
if (!accessKeyId) throw new Error("AWS_ACCESS_KEY_ID is not set");
|
|
320
|
+
if (!secretAccessKey) throw new Error("AWS_SECRET_ACCESS_KEY is not set");
|
|
321
|
+
return { region, bucket, accessKeyId, secretAccessKey };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export { createConsoleEmailPlugin, createResendPlugin, createS3Plugin, createSendGridPlugin, createStripePlugin, createVercelBlobPlugin };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payment-plugins.d.ts","sourceRoot":"","sources":["../../../src/plugin/builtin/payment-plugins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAA0B,MAAM,UAAU,CAAC;AACnE,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,sBAAsB,CAAC;AAG1F,wBAAgB,kBAAkB,IAAI,UAAU,GAAG,eAAe,CAiFjE"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { MarsPlugin } from '../index';
|
|
2
|
+
import type { StorageProvider } from '../../storage/index';
|
|
3
|
+
export declare function createVercelBlobPlugin(): MarsPlugin & StorageProvider;
|
|
4
|
+
export declare function createS3Plugin(): MarsPlugin & StorageProvider;
|
|
5
|
+
//# sourceMappingURL=storage-plugins.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage-plugins.d.ts","sourceRoot":"","sources":["../../../src/plugin/builtin/storage-plugins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAA0B,MAAM,UAAU,CAAC;AACnE,OAAO,KAAK,EAAE,eAAe,EAA8B,MAAM,qBAAqB,CAAC;AAGvF,wBAAgB,sBAAsB,IAAI,UAAU,GAAG,eAAe,CA2CrE;AAED,wBAAgB,cAAc,IAAI,UAAU,GAAG,eAAe,CAkH7D"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type PluginType = 'email' | 'payment' | 'storage' | 'auth' | 'analytics' | 'monitoring' | 'search' | 'realtime' | 'jobs';
|
|
2
|
+
export interface MarsPlugin<TConfig = unknown> {
|
|
3
|
+
name: string;
|
|
4
|
+
version: string;
|
|
5
|
+
type: PluginType;
|
|
6
|
+
configure(config: TConfig): void;
|
|
7
|
+
validate?(): PluginValidationResult;
|
|
8
|
+
}
|
|
9
|
+
export interface PluginValidationResult {
|
|
10
|
+
valid: boolean;
|
|
11
|
+
errors: string[];
|
|
12
|
+
warnings: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface PluginRegistry {
|
|
15
|
+
register<T>(plugin: MarsPlugin<T>): void;
|
|
16
|
+
get(type: string, name: string): MarsPlugin | undefined;
|
|
17
|
+
getByType(type: string): MarsPlugin[];
|
|
18
|
+
validate(): PluginValidationResult;
|
|
19
|
+
}
|
|
20
|
+
export declare function createPluginRegistry(): PluginRegistry;
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugin/index.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAClB,OAAO,GACP,SAAS,GACT,SAAS,GACT,MAAM,GACN,WAAW,GACX,YAAY,GACZ,QAAQ,GACR,UAAU,GACV,MAAM,CAAC;AAEX,MAAM,WAAW,UAAU,CAAC,OAAO,GAAG,OAAO;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IACjC,QAAQ,CAAC,IAAI,sBAAsB,CAAC;CACrC;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;IACzC,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;IACxD,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,EAAE,CAAC;IACtC,QAAQ,IAAI,sBAAsB,CAAC;CACpC;AAED,wBAAgB,oBAAoB,IAAI,cAAc,CAgCrD"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/plugin/index.ts
|
|
2
|
+
function createPluginRegistry() {
|
|
3
|
+
const plugins = /* @__PURE__ */ new Map();
|
|
4
|
+
return {
|
|
5
|
+
register(plugin) {
|
|
6
|
+
const key = `${plugin.type}:${plugin.name}`;
|
|
7
|
+
plugins.set(key, plugin);
|
|
8
|
+
},
|
|
9
|
+
get(type, name) {
|
|
10
|
+
return plugins.get(`${type}:${name}`);
|
|
11
|
+
},
|
|
12
|
+
getByType(type) {
|
|
13
|
+
return Array.from(plugins.values()).filter((p) => p.type === type);
|
|
14
|
+
},
|
|
15
|
+
validate() {
|
|
16
|
+
const errors = [];
|
|
17
|
+
const warnings = [];
|
|
18
|
+
for (const plugin of plugins.values()) {
|
|
19
|
+
if (plugin.validate) {
|
|
20
|
+
const result = plugin.validate();
|
|
21
|
+
errors.push(...result.errors.map((e) => `[${plugin.name}] ${e}`));
|
|
22
|
+
warnings.push(...result.warnings.map((w) => `[${plugin.name}] ${w}`));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { createPluginRegistry };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
export interface RateLimitConfig {
|
|
4
|
+
limit: number;
|
|
5
|
+
windowSeconds: number;
|
|
6
|
+
identifier: string;
|
|
7
|
+
/** Use sliding window instead of fixed window. Recommended for auth endpoints. */
|
|
8
|
+
sliding?: boolean;
|
|
9
|
+
}
|
|
10
|
+
interface RateLimitResult {
|
|
11
|
+
success: boolean;
|
|
12
|
+
remaining: number;
|
|
13
|
+
resetAt: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Checks whether a request from the given IP is within the rate limit.
|
|
17
|
+
* Tries Upstash Redis first, falling back to an in-memory store.
|
|
18
|
+
*
|
|
19
|
+
* @param ip - The client IP address
|
|
20
|
+
* @param config - Rate limit configuration (limit, window, identifier, sliding)
|
|
21
|
+
* @returns A result indicating success, remaining requests, and reset timestamp
|
|
22
|
+
* @example
|
|
23
|
+
* const result = await checkRateLimit(clientIp, RATE_LIMITS.login);
|
|
24
|
+
* if (!result.success) return rateLimitResponse(result.resetAt);
|
|
25
|
+
*/
|
|
26
|
+
export declare function checkRateLimit(ip: string, config: RateLimitConfig): Promise<RateLimitResult>;
|
|
27
|
+
export interface GetClientIPOptions {
|
|
28
|
+
/** Index into x-forwarded-for (0 = leftmost/client, 1 = first proxy). Default 0. */
|
|
29
|
+
trustProxyDepth?: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Extracts the client IP address from request headers, supporting proxied environments.
|
|
33
|
+
* Reads `x-forwarded-for` (respecting proxy depth) then falls back to `x-real-ip`.
|
|
34
|
+
*
|
|
35
|
+
* @param request - The incoming Request object
|
|
36
|
+
* @param options - Options controlling proxy trust depth
|
|
37
|
+
* @returns The client IP string, or 'unknown' if not determinable
|
|
38
|
+
* @example
|
|
39
|
+
* const ip = getClientIP(request, { trustProxyDepth: 0 });
|
|
40
|
+
*/
|
|
41
|
+
export declare function getClientIP(request: Request, options?: GetClientIPOptions): string;
|
|
42
|
+
/**
|
|
43
|
+
* Builds a 429 Too Many Requests response with standard rate-limit headers.
|
|
44
|
+
*
|
|
45
|
+
* @param resetAt - Unix timestamp (ms) when the rate limit window resets
|
|
46
|
+
* @returns A NextResponse with status 429, Retry-After, and X-RateLimit-* headers
|
|
47
|
+
* @example
|
|
48
|
+
* if (!result.success) return rateLimitResponse(result.resetAt);
|
|
49
|
+
*/
|
|
50
|
+
export declare function rateLimitResponse(resetAt: number): NextResponse;
|
|
51
|
+
export declare const RATE_LIMITS: {
|
|
52
|
+
readonly login: {
|
|
53
|
+
readonly limit: 5;
|
|
54
|
+
readonly windowSeconds: 60;
|
|
55
|
+
readonly identifier: "login";
|
|
56
|
+
readonly sliding: true;
|
|
57
|
+
};
|
|
58
|
+
readonly signup: {
|
|
59
|
+
readonly limit: 3;
|
|
60
|
+
readonly windowSeconds: 60;
|
|
61
|
+
readonly identifier: "signup";
|
|
62
|
+
readonly sliding: true;
|
|
63
|
+
};
|
|
64
|
+
readonly forgotPassword: {
|
|
65
|
+
readonly limit: 3;
|
|
66
|
+
readonly windowSeconds: 300;
|
|
67
|
+
readonly identifier: "forgot-password";
|
|
68
|
+
readonly sliding: true;
|
|
69
|
+
};
|
|
70
|
+
readonly resetPassword: {
|
|
71
|
+
readonly limit: 5;
|
|
72
|
+
readonly windowSeconds: 300;
|
|
73
|
+
readonly identifier: "reset-password";
|
|
74
|
+
readonly sliding: true;
|
|
75
|
+
};
|
|
76
|
+
readonly emailVerification: {
|
|
77
|
+
readonly limit: 5;
|
|
78
|
+
readonly windowSeconds: 300;
|
|
79
|
+
readonly identifier: "email-verification";
|
|
80
|
+
readonly sliding: true;
|
|
81
|
+
};
|
|
82
|
+
readonly api: {
|
|
83
|
+
readonly limit: 60;
|
|
84
|
+
readonly windowSeconds: 60;
|
|
85
|
+
readonly identifier: "api";
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
export {};
|
|
89
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rate-limit/index.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAErB,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAkC3C,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,kFAAkF;IAClF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,UAAU,eAAe;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAqED;;;;;;;;;;GAUG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,eAAe,GACtB,OAAO,CAAC,eAAe,CAAC,CAuC1B;AAED,MAAM,WAAW,kBAAkB;IACjC,oFAAoF;IACpF,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,kBAAuB,GAC/B,MAAM,CAYR;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,CAa/D;AAED,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsBd,CAAC"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import { Ratelimit } from '@upstash/ratelimit';
|
|
4
|
+
import { Redis } from '@upstash/redis';
|
|
5
|
+
|
|
6
|
+
// src/rate-limit/index.ts
|
|
7
|
+
var MAX_IN_MEMORY_STORE_SIZE = 1e4;
|
|
8
|
+
var rateLimitStore = /* @__PURE__ */ new Map();
|
|
9
|
+
var lastCleanup = Date.now();
|
|
10
|
+
var CLEANUP_INTERVAL_MS = 6e4;
|
|
11
|
+
function evictOldestIfNeeded() {
|
|
12
|
+
if (rateLimitStore.size < MAX_IN_MEMORY_STORE_SIZE) return;
|
|
13
|
+
const entries = [...rateLimitStore.entries()].sort(([, a], [, b]) => a.resetAt - b.resetAt);
|
|
14
|
+
const toRemove = rateLimitStore.size - Math.floor(MAX_IN_MEMORY_STORE_SIZE * 0.9);
|
|
15
|
+
for (let i = 0; i < toRemove && i < entries.length; i++) {
|
|
16
|
+
rateLimitStore.delete(entries[i][0]);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function lazyCleanup() {
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
if (now - lastCleanup < CLEANUP_INTERVAL_MS) return;
|
|
22
|
+
lastCleanup = now;
|
|
23
|
+
for (const [key, entry] of rateLimitStore.entries()) {
|
|
24
|
+
if (now > entry.resetAt) {
|
|
25
|
+
rateLimitStore.delete(key);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
var hasWarnedMissingUpstashConfig = false;
|
|
30
|
+
var hasWarnedUpstashFailure = false;
|
|
31
|
+
var upstashRedisClient;
|
|
32
|
+
var upstashLimiters = /* @__PURE__ */ new Map();
|
|
33
|
+
function getUpstashRedisConfig() {
|
|
34
|
+
const url = process.env.UPSTASH_REDIS_REST_URL || process.env.KV_REST_API_URL;
|
|
35
|
+
const token = process.env.UPSTASH_REDIS_REST_TOKEN || process.env.KV_REST_API_TOKEN;
|
|
36
|
+
if (!url || !token) return null;
|
|
37
|
+
return { url, token };
|
|
38
|
+
}
|
|
39
|
+
function getUpstashRedisClient() {
|
|
40
|
+
if (upstashRedisClient !== void 0) return upstashRedisClient;
|
|
41
|
+
const config = getUpstashRedisConfig();
|
|
42
|
+
if (!config) {
|
|
43
|
+
if (process.env.NODE_ENV === "production" && !hasWarnedMissingUpstashConfig) {
|
|
44
|
+
hasWarnedMissingUpstashConfig = true;
|
|
45
|
+
console.error(
|
|
46
|
+
"SECURITY WARNING: Rate limiting is using in-memory fallback in production. This is NOT effective in serverless environments. Configure UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN."
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
upstashRedisClient = null;
|
|
50
|
+
return upstashRedisClient;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
upstashRedisClient = new Redis({ url: config.url, token: config.token });
|
|
54
|
+
return upstashRedisClient;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (!hasWarnedUpstashFailure) {
|
|
57
|
+
hasWarnedUpstashFailure = true;
|
|
58
|
+
console.error("Failed to initialize Upstash Redis. Falling back to in-memory limiter.", error);
|
|
59
|
+
}
|
|
60
|
+
upstashRedisClient = null;
|
|
61
|
+
return upstashRedisClient;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function normalizeUpstashReset(reset) {
|
|
65
|
+
return reset < 1e12 ? reset * 1e3 : reset;
|
|
66
|
+
}
|
|
67
|
+
function getUpstashLimiter(config) {
|
|
68
|
+
const redis = getUpstashRedisClient();
|
|
69
|
+
if (!redis) return null;
|
|
70
|
+
const limiterKey = `${config.identifier}:${config.limit}:${config.windowSeconds}`;
|
|
71
|
+
const existing = upstashLimiters.get(limiterKey);
|
|
72
|
+
if (existing) return existing;
|
|
73
|
+
const window = `${config.windowSeconds} s`;
|
|
74
|
+
const limiter = new Ratelimit({
|
|
75
|
+
redis,
|
|
76
|
+
limiter: config.sliding ? Ratelimit.slidingWindow(config.limit, window) : Ratelimit.fixedWindow(config.limit, window),
|
|
77
|
+
prefix: `ratelimit:${config.identifier}`
|
|
78
|
+
});
|
|
79
|
+
upstashLimiters.set(limiterKey, limiter);
|
|
80
|
+
return limiter;
|
|
81
|
+
}
|
|
82
|
+
async function checkRateLimit(ip, config) {
|
|
83
|
+
lazyCleanup();
|
|
84
|
+
evictOldestIfNeeded();
|
|
85
|
+
const upstashLimiter = getUpstashLimiter(config);
|
|
86
|
+
if (upstashLimiter) {
|
|
87
|
+
try {
|
|
88
|
+
const result = await upstashLimiter.limit(ip);
|
|
89
|
+
return {
|
|
90
|
+
success: result.success,
|
|
91
|
+
remaining: Math.max(0, result.remaining),
|
|
92
|
+
resetAt: normalizeUpstashReset(result.reset)
|
|
93
|
+
};
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (!hasWarnedUpstashFailure) {
|
|
96
|
+
hasWarnedUpstashFailure = true;
|
|
97
|
+
console.error("Upstash rate limit check failed. Falling back to in-memory limiter.", error);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const effectiveIp = ip === "unknown" ? `unknown:${Math.floor(Date.now() / 6e4)}` : ip;
|
|
102
|
+
const key = `${config.identifier}:${effectiveIp}`;
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
const windowMs = config.windowSeconds * 1e3;
|
|
105
|
+
const existing = rateLimitStore.get(key);
|
|
106
|
+
if (!existing || now > existing.resetAt) {
|
|
107
|
+
rateLimitStore.set(key, { count: 1, resetAt: now + windowMs });
|
|
108
|
+
return { success: true, remaining: config.limit - 1, resetAt: now + windowMs };
|
|
109
|
+
}
|
|
110
|
+
if (existing.count >= config.limit) {
|
|
111
|
+
return { success: false, remaining: 0, resetAt: existing.resetAt };
|
|
112
|
+
}
|
|
113
|
+
existing.count++;
|
|
114
|
+
rateLimitStore.set(key, existing);
|
|
115
|
+
return { success: true, remaining: config.limit - existing.count, resetAt: existing.resetAt };
|
|
116
|
+
}
|
|
117
|
+
function getClientIP(request, options = {}) {
|
|
118
|
+
const forwarded = request.headers.get("x-forwarded-for");
|
|
119
|
+
if (forwarded) {
|
|
120
|
+
const parts = forwarded.split(",").map((p) => p.trim());
|
|
121
|
+
const index = Math.min(options.trustProxyDepth ?? 0, parts.length - 1);
|
|
122
|
+
return parts[Math.max(0, index)] ?? "unknown";
|
|
123
|
+
}
|
|
124
|
+
const realIp = request.headers.get("x-real-ip");
|
|
125
|
+
if (realIp) return realIp.trim();
|
|
126
|
+
return "unknown";
|
|
127
|
+
}
|
|
128
|
+
function rateLimitResponse(resetAt) {
|
|
129
|
+
const retryAfter = Math.ceil((resetAt - Date.now()) / 1e3);
|
|
130
|
+
return NextResponse.json(
|
|
131
|
+
{ error: "Too many requests. Please try again later.", retryAfter },
|
|
132
|
+
{
|
|
133
|
+
status: 429,
|
|
134
|
+
headers: {
|
|
135
|
+
"Retry-After": String(retryAfter),
|
|
136
|
+
"X-RateLimit-Remaining": "0",
|
|
137
|
+
"X-RateLimit-Reset": String(Math.ceil(resetAt / 1e3))
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
var RATE_LIMITS = {
|
|
143
|
+
login: { limit: 5, windowSeconds: 60, identifier: "login", sliding: true },
|
|
144
|
+
signup: { limit: 3, windowSeconds: 60, identifier: "signup", sliding: true },
|
|
145
|
+
forgotPassword: {
|
|
146
|
+
limit: 3,
|
|
147
|
+
windowSeconds: 300,
|
|
148
|
+
identifier: "forgot-password",
|
|
149
|
+
sliding: true
|
|
150
|
+
},
|
|
151
|
+
resetPassword: {
|
|
152
|
+
limit: 5,
|
|
153
|
+
windowSeconds: 300,
|
|
154
|
+
identifier: "reset-password",
|
|
155
|
+
sliding: true
|
|
156
|
+
},
|
|
157
|
+
emailVerification: {
|
|
158
|
+
limit: 5,
|
|
159
|
+
windowSeconds: 300,
|
|
160
|
+
identifier: "email-verification",
|
|
161
|
+
sliding: true
|
|
162
|
+
},
|
|
163
|
+
api: { limit: 60, windowSeconds: 60, identifier: "api" }
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
export { RATE_LIMITS, checkRateLimit, getClientIP, rateLimitResponse };
|