@codaijs/keel 0.2.2 → 0.2.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/dist/__tests__/sail-installer.test.js +25 -25
- package/dist/sail-installer.js +174 -174
- package/dist/scaffold.js +68 -68
- package/package.json +58 -58
- package/sails/_template/addon.json +20 -20
- package/sails/_template/install.ts +402 -402
- package/sails/admin-dashboard/README.md +117 -117
- package/sails/admin-dashboard/addon.json +28 -28
- package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -34
- package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -243
- package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -40
- package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -240
- package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -149
- package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -173
- package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -203
- package/sails/admin-dashboard/install.ts +305 -305
- package/sails/analytics/README.md +178 -178
- package/sails/analytics/addon.json +27 -27
- package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -58
- package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -64
- package/sails/analytics/files/frontend/lib/analytics.ts +103 -103
- package/sails/analytics/install.ts +297 -297
- package/sails/file-uploads/addon.json +30 -30
- package/sails/file-uploads/files/backend/routes/files.ts +198 -198
- package/sails/file-uploads/files/backend/schema/files.ts +36 -36
- package/sails/file-uploads/files/backend/services/file-storage.ts +128 -128
- package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -248
- package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -147
- package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -106
- package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -118
- package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -37
- package/sails/file-uploads/install.ts +466 -466
- package/sails/gdpr/README.md +174 -174
- package/sails/gdpr/addon.json +27 -27
- package/sails/gdpr/files/backend/routes/gdpr.ts +140 -140
- package/sails/gdpr/files/backend/services/gdpr.ts +293 -293
- package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -97
- package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -192
- package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -75
- package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -186
- package/sails/gdpr/install.ts +756 -756
- package/sails/google-oauth/README.md +121 -121
- package/sails/google-oauth/addon.json +22 -22
- package/sails/google-oauth/files/GoogleButton.tsx +50 -50
- package/sails/google-oauth/install.ts +252 -252
- package/sails/i18n/README.md +193 -193
- package/sails/i18n/addon.json +30 -30
- package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -108
- package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -31
- package/sails/i18n/files/frontend/lib/i18n.ts +32 -32
- package/sails/i18n/files/frontend/locales/de/common.json +44 -44
- package/sails/i18n/files/frontend/locales/en/common.json +44 -44
- package/sails/i18n/install.ts +407 -407
- package/sails/push-notifications/README.md +163 -163
- package/sails/push-notifications/addon.json +31 -31
- package/sails/push-notifications/files/backend/routes/notifications.ts +153 -153
- package/sails/push-notifications/files/backend/schema/notifications.ts +31 -31
- package/sails/push-notifications/files/backend/services/notifications.ts +117 -117
- package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -12
- package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -154
- package/sails/push-notifications/install.ts +384 -384
- package/sails/r2-storage/addon.json +29 -29
- package/sails/r2-storage/files/backend/services/storage.ts +71 -71
- package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -167
- package/sails/r2-storage/install.ts +412 -412
- package/sails/rate-limiting/addon.json +20 -20
- package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -104
- package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -137
- package/sails/rate-limiting/install.ts +300 -300
- package/sails/registry.json +107 -107
- package/sails/stripe/README.md +214 -214
- package/sails/stripe/addon.json +24 -24
- package/sails/stripe/files/backend/routes/stripe.ts +154 -154
- package/sails/stripe/files/backend/schema/stripe.ts +74 -74
- package/sails/stripe/files/backend/services/stripe.ts +224 -224
- package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -135
- package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -86
- package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -116
- package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -226
- package/sails/stripe/install.ts +378 -378
|
@@ -1,300 +1,300 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* API Rate Limiting Sail Installer
|
|
3
|
-
*
|
|
4
|
-
* Adds in-memory sliding window rate limiting to your API routes.
|
|
5
|
-
* No external dependencies required.
|
|
6
|
-
*
|
|
7
|
-
* Usage:
|
|
8
|
-
* npx tsx sails/rate-limiting/install.ts
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
readFileSync,
|
|
13
|
-
writeFileSync,
|
|
14
|
-
copyFileSync,
|
|
15
|
-
existsSync,
|
|
16
|
-
mkdirSync,
|
|
17
|
-
} from "node:fs";
|
|
18
|
-
import { resolve, dirname, join } from "node:path";
|
|
19
|
-
import { input, confirm, select } from "@inquirer/prompts";
|
|
20
|
-
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
// Paths
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
|
|
25
|
-
const SAIL_DIR = dirname(new URL(import.meta.url).pathname);
|
|
26
|
-
const PROJECT_ROOT = resolve(SAIL_DIR, "../..");
|
|
27
|
-
const BACKEND_ROOT = join(PROJECT_ROOT, "packages/backend");
|
|
28
|
-
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
// Helpers
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
|
|
33
|
-
interface SailManifest {
|
|
34
|
-
name: string;
|
|
35
|
-
displayName: string;
|
|
36
|
-
version: string;
|
|
37
|
-
requiredEnvVars: { key: string; description: string }[];
|
|
38
|
-
dependencies: { backend: Record<string, string>; frontend: Record<string, string> };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function loadManifest(): SailManifest {
|
|
42
|
-
return JSON.parse(readFileSync(join(SAIL_DIR, "addon.json"), "utf-8"));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function insertAtMarker(filePath: string, marker: string, code: string): void {
|
|
46
|
-
if (!existsSync(filePath)) {
|
|
47
|
-
console.warn(` Warning: File not found: ${filePath}`);
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
let content = readFileSync(filePath, "utf-8");
|
|
51
|
-
if (!content.includes(marker)) {
|
|
52
|
-
console.warn(` Warning: Marker "${marker}" not found in ${filePath}`);
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
if (content.includes(code.trim())) {
|
|
56
|
-
console.log(` Skipped (already present) -> ${filePath}`);
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
content = content.replace(marker, `${marker}\n${code}`);
|
|
60
|
-
writeFileSync(filePath, content, "utf-8");
|
|
61
|
-
console.log(` Modified -> ${filePath}`);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function copyFile(src: string, dest: string, label: string): void {
|
|
65
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
66
|
-
copyFileSync(src, dest);
|
|
67
|
-
console.log(` Copied -> ${label}`);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function appendToEnvFiles(entries: Record<string, string>, section: string): void {
|
|
71
|
-
for (const envFile of [".env.example", ".env"]) {
|
|
72
|
-
const envPath = join(PROJECT_ROOT, envFile);
|
|
73
|
-
if (!existsSync(envPath)) continue;
|
|
74
|
-
let content = readFileSync(envPath, "utf-8");
|
|
75
|
-
const lines: string[] = [];
|
|
76
|
-
for (const [key, val] of Object.entries(entries)) {
|
|
77
|
-
if (!content.includes(key)) lines.push(`${key}=${val}`);
|
|
78
|
-
}
|
|
79
|
-
if (lines.length > 0) {
|
|
80
|
-
content += `\n# ${section}\n${lines.join("\n")}\n`;
|
|
81
|
-
writeFileSync(envPath, content, "utf-8");
|
|
82
|
-
console.log(` Updated ${envFile}`);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ---------------------------------------------------------------------------
|
|
88
|
-
// Main
|
|
89
|
-
// ---------------------------------------------------------------------------
|
|
90
|
-
|
|
91
|
-
async function main(): Promise<void> {
|
|
92
|
-
const manifest = loadManifest();
|
|
93
|
-
|
|
94
|
-
// -- Step 1: Welcome -------------------------------------------------------
|
|
95
|
-
console.log("\n------------------------------------------------------------");
|
|
96
|
-
console.log(` API Rate Limiting Sail Installer (v${manifest.version})`);
|
|
97
|
-
console.log("------------------------------------------------------------");
|
|
98
|
-
console.log();
|
|
99
|
-
console.log(" This sail adds rate limiting to protect your API endpoints:");
|
|
100
|
-
console.log(" - In-memory sliding window algorithm (no Redis needed)");
|
|
101
|
-
console.log(" - Per-IP or per-user request tracking");
|
|
102
|
-
console.log(" - Preset limiters for auth, general API, and sensitive routes");
|
|
103
|
-
console.log(" - Automatic 429 Too Many Requests with Retry-After header");
|
|
104
|
-
console.log(" - Periodic cleanup of expired entries");
|
|
105
|
-
console.log();
|
|
106
|
-
|
|
107
|
-
const pkgPath = join(PROJECT_ROOT, "package.json");
|
|
108
|
-
if (existsSync(pkgPath)) {
|
|
109
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
110
|
-
console.log(` Template version: ${pkg.version ?? "unknown"}`);
|
|
111
|
-
console.log();
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// -- Step 2: Configure defaults -------------------------------------------
|
|
115
|
-
console.log(" Configure rate limit defaults:");
|
|
116
|
-
console.log(" (You can always adjust these later in your code or via env vars)");
|
|
117
|
-
console.log();
|
|
118
|
-
|
|
119
|
-
const windowInput = await input({
|
|
120
|
-
message: "Default rate limit window in minutes:",
|
|
121
|
-
default: "15",
|
|
122
|
-
validate: (value) => {
|
|
123
|
-
const n = Number(value);
|
|
124
|
-
if (isNaN(n) || n <= 0) return "Please enter a positive number.";
|
|
125
|
-
return true;
|
|
126
|
-
},
|
|
127
|
-
});
|
|
128
|
-
const windowMinutes = Number(windowInput);
|
|
129
|
-
|
|
130
|
-
const maxRequestsInput = await input({
|
|
131
|
-
message: "Default max requests per window:",
|
|
132
|
-
default: "100",
|
|
133
|
-
validate: (value) => {
|
|
134
|
-
const n = Number(value);
|
|
135
|
-
if (isNaN(n) || n <= 0) return "Please enter a positive number.";
|
|
136
|
-
return true;
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
|
-
const maxRequests = Number(maxRequestsInput);
|
|
140
|
-
|
|
141
|
-
// -- Step 3: Choose protection scope --------------------------------------
|
|
142
|
-
console.log();
|
|
143
|
-
|
|
144
|
-
const scope = await select({
|
|
145
|
-
message: "Which routes should be rate limited?",
|
|
146
|
-
choices: [
|
|
147
|
-
{
|
|
148
|
-
name: "All API routes (recommended)",
|
|
149
|
-
value: "all",
|
|
150
|
-
description: "Apply apiLimiter globally + authLimiter on auth routes",
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
name: "Auth routes only",
|
|
154
|
-
value: "auth-only",
|
|
155
|
-
description: "Only protect login, signup, and password reset endpoints",
|
|
156
|
-
},
|
|
157
|
-
{
|
|
158
|
-
name: "Custom (I will configure manually)",
|
|
159
|
-
value: "custom",
|
|
160
|
-
description: "Copy the middleware files; you wire them up yourself",
|
|
161
|
-
},
|
|
162
|
-
],
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// -- Step 4: Summary ------------------------------------------------------
|
|
166
|
-
console.log();
|
|
167
|
-
console.log(" Summary of changes:");
|
|
168
|
-
console.log(" -------------------");
|
|
169
|
-
console.log(" Files to create:");
|
|
170
|
-
console.log(" + packages/backend/src/middleware/rate-limit.ts");
|
|
171
|
-
console.log(" + packages/backend/src/middleware/rate-limit-store.ts");
|
|
172
|
-
console.log();
|
|
173
|
-
console.log(" Files to modify:");
|
|
174
|
-
|
|
175
|
-
if (scope !== "custom") {
|
|
176
|
-
console.log(" ~ packages/backend/src/index.ts (import + apply middleware)");
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (windowMinutes !== 15 || maxRequests !== 100) {
|
|
180
|
-
console.log(" ~ packages/backend/src/env.ts (add optional env vars)");
|
|
181
|
-
console.log(" ~ .env.example / .env");
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
console.log();
|
|
185
|
-
console.log(" Configuration:");
|
|
186
|
-
console.log(` Window: ${windowMinutes} minutes`);
|
|
187
|
-
console.log(` Max requests: ${maxRequests} per window`);
|
|
188
|
-
console.log(` Scope: ${scope === "all" ? "All API routes" : scope === "auth-only" ? "Auth routes only" : "Manual"}`);
|
|
189
|
-
console.log();
|
|
190
|
-
|
|
191
|
-
// -- Step 5: Confirm ------------------------------------------------------
|
|
192
|
-
const proceed = await confirm({ message: "Proceed with installation?", default: true });
|
|
193
|
-
if (!proceed) {
|
|
194
|
-
console.log("\n Installation cancelled.\n");
|
|
195
|
-
process.exit(0);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// -- Step 6: Execute -------------------------------------------------------
|
|
199
|
-
console.log();
|
|
200
|
-
console.log(" Installing...");
|
|
201
|
-
console.log();
|
|
202
|
-
|
|
203
|
-
console.log(" Copying backend files...");
|
|
204
|
-
copyFile(
|
|
205
|
-
join(SAIL_DIR, "files/backend/middleware/rate-limit-store.ts"),
|
|
206
|
-
join(BACKEND_ROOT, "src/middleware/rate-limit-store.ts"),
|
|
207
|
-
"src/middleware/rate-limit-store.ts",
|
|
208
|
-
);
|
|
209
|
-
copyFile(
|
|
210
|
-
join(SAIL_DIR, "files/backend/middleware/rate-limit.ts"),
|
|
211
|
-
join(BACKEND_ROOT, "src/middleware/rate-limit.ts"),
|
|
212
|
-
"src/middleware/rate-limit.ts",
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
console.log();
|
|
216
|
-
console.log(" Modifying backend files...");
|
|
217
|
-
|
|
218
|
-
// Insert imports and route middleware based on scope
|
|
219
|
-
if (scope === "all") {
|
|
220
|
-
insertAtMarker(
|
|
221
|
-
join(BACKEND_ROOT, "src/index.ts"),
|
|
222
|
-
"// [SAIL_IMPORTS]",
|
|
223
|
-
'import { apiLimiter, authLimiter } from "./middleware/rate-limit.js";',
|
|
224
|
-
);
|
|
225
|
-
insertAtMarker(
|
|
226
|
-
join(BACKEND_ROOT, "src/index.ts"),
|
|
227
|
-
"// [SAIL_ROUTES]",
|
|
228
|
-
'// Rate limiting\napp.use("/api/auth", authLimiter);\napp.use("/api", apiLimiter);',
|
|
229
|
-
);
|
|
230
|
-
} else if (scope === "auth-only") {
|
|
231
|
-
insertAtMarker(
|
|
232
|
-
join(BACKEND_ROOT, "src/index.ts"),
|
|
233
|
-
"// [SAIL_IMPORTS]",
|
|
234
|
-
'import { authLimiter } from "./middleware/rate-limit.js";',
|
|
235
|
-
);
|
|
236
|
-
insertAtMarker(
|
|
237
|
-
join(BACKEND_ROOT, "src/index.ts"),
|
|
238
|
-
"// [SAIL_ROUTES]",
|
|
239
|
-
'// Rate limiting (auth routes)\napp.use("/api/auth", authLimiter);',
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
// scope === "custom": no marker insertions
|
|
243
|
-
|
|
244
|
-
// Add optional env vars if defaults were customised
|
|
245
|
-
if (windowMinutes !== 15 || maxRequests !== 100) {
|
|
246
|
-
insertAtMarker(
|
|
247
|
-
join(BACKEND_ROOT, "src/env.ts"),
|
|
248
|
-
"// [SAIL_ENV_VARS]",
|
|
249
|
-
` RATE_LIMIT_WINDOW_MS: z.coerce.number().default(${windowMinutes * 60 * 1000}),\n RATE_LIMIT_MAX_REQUESTS: z.coerce.number().default(${maxRequests}),`,
|
|
250
|
-
);
|
|
251
|
-
|
|
252
|
-
console.log();
|
|
253
|
-
console.log(" Updating environment files...");
|
|
254
|
-
appendToEnvFiles(
|
|
255
|
-
{
|
|
256
|
-
RATE_LIMIT_WINDOW_MS: String(windowMinutes * 60 * 1000),
|
|
257
|
-
RATE_LIMIT_MAX_REQUESTS: String(maxRequests),
|
|
258
|
-
},
|
|
259
|
-
"Rate Limiting",
|
|
260
|
-
);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// -- Step 7: Next steps ----------------------------------------------------
|
|
264
|
-
console.log();
|
|
265
|
-
console.log("------------------------------------------------------------");
|
|
266
|
-
console.log(" API Rate Limiting installed successfully!");
|
|
267
|
-
console.log("------------------------------------------------------------");
|
|
268
|
-
console.log();
|
|
269
|
-
console.log(" Next steps:");
|
|
270
|
-
console.log();
|
|
271
|
-
console.log(" 1. Start your dev server:");
|
|
272
|
-
console.log(" npm run dev");
|
|
273
|
-
console.log();
|
|
274
|
-
console.log(" 2. Test rate limiting:");
|
|
275
|
-
console.log(" Make rapid requests to an API endpoint and verify");
|
|
276
|
-
console.log(" you get a 429 response after the limit is exceeded.");
|
|
277
|
-
console.log();
|
|
278
|
-
console.log(" Customising per-route limits:");
|
|
279
|
-
console.log();
|
|
280
|
-
console.log(' import { createRateLimiter } from "./middleware/rate-limit.js";');
|
|
281
|
-
console.log();
|
|
282
|
-
console.log(" const uploadLimiter = createRateLimiter({");
|
|
283
|
-
console.log(" windowMs: 60 * 60 * 1000, // 1 hour");
|
|
284
|
-
console.log(" maxRequests: 20,");
|
|
285
|
-
console.log(" });");
|
|
286
|
-
console.log(' app.use("/api/uploads", uploadLimiter);');
|
|
287
|
-
console.log();
|
|
288
|
-
console.log(" Swapping to Redis (distributed deployments):");
|
|
289
|
-
console.log();
|
|
290
|
-
console.log(" Implement the RateLimitStore interface from");
|
|
291
|
-
console.log(" rate-limit-store.ts and pass it as the `store` option:");
|
|
292
|
-
console.log();
|
|
293
|
-
console.log(" createRateLimiter({ store: new RedisStore(redisClient) });");
|
|
294
|
-
console.log();
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
main().catch((err) => {
|
|
298
|
-
console.error("Installation failed:", err);
|
|
299
|
-
process.exit(1);
|
|
300
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* API Rate Limiting Sail Installer
|
|
3
|
+
*
|
|
4
|
+
* Adds in-memory sliding window rate limiting to your API routes.
|
|
5
|
+
* No external dependencies required.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* npx tsx sails/rate-limiting/install.ts
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
readFileSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
copyFileSync,
|
|
15
|
+
existsSync,
|
|
16
|
+
mkdirSync,
|
|
17
|
+
} from "node:fs";
|
|
18
|
+
import { resolve, dirname, join } from "node:path";
|
|
19
|
+
import { input, confirm, select } from "@inquirer/prompts";
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Paths
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
const SAIL_DIR = dirname(new URL(import.meta.url).pathname);
|
|
26
|
+
const PROJECT_ROOT = resolve(SAIL_DIR, "../..");
|
|
27
|
+
const BACKEND_ROOT = join(PROJECT_ROOT, "packages/backend");
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Helpers
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
interface SailManifest {
|
|
34
|
+
name: string;
|
|
35
|
+
displayName: string;
|
|
36
|
+
version: string;
|
|
37
|
+
requiredEnvVars: { key: string; description: string }[];
|
|
38
|
+
dependencies: { backend: Record<string, string>; frontend: Record<string, string> };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function loadManifest(): SailManifest {
|
|
42
|
+
return JSON.parse(readFileSync(join(SAIL_DIR, "addon.json"), "utf-8"));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function insertAtMarker(filePath: string, marker: string, code: string): void {
|
|
46
|
+
if (!existsSync(filePath)) {
|
|
47
|
+
console.warn(` Warning: File not found: ${filePath}`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
let content = readFileSync(filePath, "utf-8");
|
|
51
|
+
if (!content.includes(marker)) {
|
|
52
|
+
console.warn(` Warning: Marker "${marker}" not found in ${filePath}`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (content.includes(code.trim())) {
|
|
56
|
+
console.log(` Skipped (already present) -> ${filePath}`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
content = content.replace(marker, `${marker}\n${code}`);
|
|
60
|
+
writeFileSync(filePath, content, "utf-8");
|
|
61
|
+
console.log(` Modified -> ${filePath}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function copyFile(src: string, dest: string, label: string): void {
|
|
65
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
66
|
+
copyFileSync(src, dest);
|
|
67
|
+
console.log(` Copied -> ${label}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function appendToEnvFiles(entries: Record<string, string>, section: string): void {
|
|
71
|
+
for (const envFile of [".env.example", ".env"]) {
|
|
72
|
+
const envPath = join(PROJECT_ROOT, envFile);
|
|
73
|
+
if (!existsSync(envPath)) continue;
|
|
74
|
+
let content = readFileSync(envPath, "utf-8");
|
|
75
|
+
const lines: string[] = [];
|
|
76
|
+
for (const [key, val] of Object.entries(entries)) {
|
|
77
|
+
if (!content.includes(key)) lines.push(`${key}=${val}`);
|
|
78
|
+
}
|
|
79
|
+
if (lines.length > 0) {
|
|
80
|
+
content += `\n# ${section}\n${lines.join("\n")}\n`;
|
|
81
|
+
writeFileSync(envPath, content, "utf-8");
|
|
82
|
+
console.log(` Updated ${envFile}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Main
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
async function main(): Promise<void> {
|
|
92
|
+
const manifest = loadManifest();
|
|
93
|
+
|
|
94
|
+
// -- Step 1: Welcome -------------------------------------------------------
|
|
95
|
+
console.log("\n------------------------------------------------------------");
|
|
96
|
+
console.log(` API Rate Limiting Sail Installer (v${manifest.version})`);
|
|
97
|
+
console.log("------------------------------------------------------------");
|
|
98
|
+
console.log();
|
|
99
|
+
console.log(" This sail adds rate limiting to protect your API endpoints:");
|
|
100
|
+
console.log(" - In-memory sliding window algorithm (no Redis needed)");
|
|
101
|
+
console.log(" - Per-IP or per-user request tracking");
|
|
102
|
+
console.log(" - Preset limiters for auth, general API, and sensitive routes");
|
|
103
|
+
console.log(" - Automatic 429 Too Many Requests with Retry-After header");
|
|
104
|
+
console.log(" - Periodic cleanup of expired entries");
|
|
105
|
+
console.log();
|
|
106
|
+
|
|
107
|
+
const pkgPath = join(PROJECT_ROOT, "package.json");
|
|
108
|
+
if (existsSync(pkgPath)) {
|
|
109
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
110
|
+
console.log(` Template version: ${pkg.version ?? "unknown"}`);
|
|
111
|
+
console.log();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// -- Step 2: Configure defaults -------------------------------------------
|
|
115
|
+
console.log(" Configure rate limit defaults:");
|
|
116
|
+
console.log(" (You can always adjust these later in your code or via env vars)");
|
|
117
|
+
console.log();
|
|
118
|
+
|
|
119
|
+
const windowInput = await input({
|
|
120
|
+
message: "Default rate limit window in minutes:",
|
|
121
|
+
default: "15",
|
|
122
|
+
validate: (value) => {
|
|
123
|
+
const n = Number(value);
|
|
124
|
+
if (isNaN(n) || n <= 0) return "Please enter a positive number.";
|
|
125
|
+
return true;
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
const windowMinutes = Number(windowInput);
|
|
129
|
+
|
|
130
|
+
const maxRequestsInput = await input({
|
|
131
|
+
message: "Default max requests per window:",
|
|
132
|
+
default: "100",
|
|
133
|
+
validate: (value) => {
|
|
134
|
+
const n = Number(value);
|
|
135
|
+
if (isNaN(n) || n <= 0) return "Please enter a positive number.";
|
|
136
|
+
return true;
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
const maxRequests = Number(maxRequestsInput);
|
|
140
|
+
|
|
141
|
+
// -- Step 3: Choose protection scope --------------------------------------
|
|
142
|
+
console.log();
|
|
143
|
+
|
|
144
|
+
const scope = await select({
|
|
145
|
+
message: "Which routes should be rate limited?",
|
|
146
|
+
choices: [
|
|
147
|
+
{
|
|
148
|
+
name: "All API routes (recommended)",
|
|
149
|
+
value: "all",
|
|
150
|
+
description: "Apply apiLimiter globally + authLimiter on auth routes",
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: "Auth routes only",
|
|
154
|
+
value: "auth-only",
|
|
155
|
+
description: "Only protect login, signup, and password reset endpoints",
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "Custom (I will configure manually)",
|
|
159
|
+
value: "custom",
|
|
160
|
+
description: "Copy the middleware files; you wire them up yourself",
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// -- Step 4: Summary ------------------------------------------------------
|
|
166
|
+
console.log();
|
|
167
|
+
console.log(" Summary of changes:");
|
|
168
|
+
console.log(" -------------------");
|
|
169
|
+
console.log(" Files to create:");
|
|
170
|
+
console.log(" + packages/backend/src/middleware/rate-limit.ts");
|
|
171
|
+
console.log(" + packages/backend/src/middleware/rate-limit-store.ts");
|
|
172
|
+
console.log();
|
|
173
|
+
console.log(" Files to modify:");
|
|
174
|
+
|
|
175
|
+
if (scope !== "custom") {
|
|
176
|
+
console.log(" ~ packages/backend/src/index.ts (import + apply middleware)");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (windowMinutes !== 15 || maxRequests !== 100) {
|
|
180
|
+
console.log(" ~ packages/backend/src/env.ts (add optional env vars)");
|
|
181
|
+
console.log(" ~ .env.example / .env");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log();
|
|
185
|
+
console.log(" Configuration:");
|
|
186
|
+
console.log(` Window: ${windowMinutes} minutes`);
|
|
187
|
+
console.log(` Max requests: ${maxRequests} per window`);
|
|
188
|
+
console.log(` Scope: ${scope === "all" ? "All API routes" : scope === "auth-only" ? "Auth routes only" : "Manual"}`);
|
|
189
|
+
console.log();
|
|
190
|
+
|
|
191
|
+
// -- Step 5: Confirm ------------------------------------------------------
|
|
192
|
+
const proceed = await confirm({ message: "Proceed with installation?", default: true });
|
|
193
|
+
if (!proceed) {
|
|
194
|
+
console.log("\n Installation cancelled.\n");
|
|
195
|
+
process.exit(0);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// -- Step 6: Execute -------------------------------------------------------
|
|
199
|
+
console.log();
|
|
200
|
+
console.log(" Installing...");
|
|
201
|
+
console.log();
|
|
202
|
+
|
|
203
|
+
console.log(" Copying backend files...");
|
|
204
|
+
copyFile(
|
|
205
|
+
join(SAIL_DIR, "files/backend/middleware/rate-limit-store.ts"),
|
|
206
|
+
join(BACKEND_ROOT, "src/middleware/rate-limit-store.ts"),
|
|
207
|
+
"src/middleware/rate-limit-store.ts",
|
|
208
|
+
);
|
|
209
|
+
copyFile(
|
|
210
|
+
join(SAIL_DIR, "files/backend/middleware/rate-limit.ts"),
|
|
211
|
+
join(BACKEND_ROOT, "src/middleware/rate-limit.ts"),
|
|
212
|
+
"src/middleware/rate-limit.ts",
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
console.log();
|
|
216
|
+
console.log(" Modifying backend files...");
|
|
217
|
+
|
|
218
|
+
// Insert imports and route middleware based on scope
|
|
219
|
+
if (scope === "all") {
|
|
220
|
+
insertAtMarker(
|
|
221
|
+
join(BACKEND_ROOT, "src/index.ts"),
|
|
222
|
+
"// [SAIL_IMPORTS]",
|
|
223
|
+
'import { apiLimiter, authLimiter } from "./middleware/rate-limit.js";',
|
|
224
|
+
);
|
|
225
|
+
insertAtMarker(
|
|
226
|
+
join(BACKEND_ROOT, "src/index.ts"),
|
|
227
|
+
"// [SAIL_ROUTES]",
|
|
228
|
+
'// Rate limiting\napp.use("/api/auth", authLimiter);\napp.use("/api", apiLimiter);',
|
|
229
|
+
);
|
|
230
|
+
} else if (scope === "auth-only") {
|
|
231
|
+
insertAtMarker(
|
|
232
|
+
join(BACKEND_ROOT, "src/index.ts"),
|
|
233
|
+
"// [SAIL_IMPORTS]",
|
|
234
|
+
'import { authLimiter } from "./middleware/rate-limit.js";',
|
|
235
|
+
);
|
|
236
|
+
insertAtMarker(
|
|
237
|
+
join(BACKEND_ROOT, "src/index.ts"),
|
|
238
|
+
"// [SAIL_ROUTES]",
|
|
239
|
+
'// Rate limiting (auth routes)\napp.use("/api/auth", authLimiter);',
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
// scope === "custom": no marker insertions
|
|
243
|
+
|
|
244
|
+
// Add optional env vars if defaults were customised
|
|
245
|
+
if (windowMinutes !== 15 || maxRequests !== 100) {
|
|
246
|
+
insertAtMarker(
|
|
247
|
+
join(BACKEND_ROOT, "src/env.ts"),
|
|
248
|
+
"// [SAIL_ENV_VARS]",
|
|
249
|
+
` RATE_LIMIT_WINDOW_MS: z.coerce.number().default(${windowMinutes * 60 * 1000}),\n RATE_LIMIT_MAX_REQUESTS: z.coerce.number().default(${maxRequests}),`,
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
console.log();
|
|
253
|
+
console.log(" Updating environment files...");
|
|
254
|
+
appendToEnvFiles(
|
|
255
|
+
{
|
|
256
|
+
RATE_LIMIT_WINDOW_MS: String(windowMinutes * 60 * 1000),
|
|
257
|
+
RATE_LIMIT_MAX_REQUESTS: String(maxRequests),
|
|
258
|
+
},
|
|
259
|
+
"Rate Limiting",
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// -- Step 7: Next steps ----------------------------------------------------
|
|
264
|
+
console.log();
|
|
265
|
+
console.log("------------------------------------------------------------");
|
|
266
|
+
console.log(" API Rate Limiting installed successfully!");
|
|
267
|
+
console.log("------------------------------------------------------------");
|
|
268
|
+
console.log();
|
|
269
|
+
console.log(" Next steps:");
|
|
270
|
+
console.log();
|
|
271
|
+
console.log(" 1. Start your dev server:");
|
|
272
|
+
console.log(" npm run dev");
|
|
273
|
+
console.log();
|
|
274
|
+
console.log(" 2. Test rate limiting:");
|
|
275
|
+
console.log(" Make rapid requests to an API endpoint and verify");
|
|
276
|
+
console.log(" you get a 429 response after the limit is exceeded.");
|
|
277
|
+
console.log();
|
|
278
|
+
console.log(" Customising per-route limits:");
|
|
279
|
+
console.log();
|
|
280
|
+
console.log(' import { createRateLimiter } from "./middleware/rate-limit.js";');
|
|
281
|
+
console.log();
|
|
282
|
+
console.log(" const uploadLimiter = createRateLimiter({");
|
|
283
|
+
console.log(" windowMs: 60 * 60 * 1000, // 1 hour");
|
|
284
|
+
console.log(" maxRequests: 20,");
|
|
285
|
+
console.log(" });");
|
|
286
|
+
console.log(' app.use("/api/uploads", uploadLimiter);');
|
|
287
|
+
console.log();
|
|
288
|
+
console.log(" Swapping to Redis (distributed deployments):");
|
|
289
|
+
console.log();
|
|
290
|
+
console.log(" Implement the RateLimitStore interface from");
|
|
291
|
+
console.log(" rate-limit-store.ts and pass it as the `store` option:");
|
|
292
|
+
console.log();
|
|
293
|
+
console.log(" createRateLimiter({ store: new RedisStore(redisClient) });");
|
|
294
|
+
console.log();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
main().catch((err) => {
|
|
298
|
+
console.error("Installation failed:", err);
|
|
299
|
+
process.exit(1);
|
|
300
|
+
});
|