@codaijs/keel 0.2.3 → 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.
Files changed (80) hide show
  1. package/dist/__tests__/sail-installer.test.js +25 -25
  2. package/dist/sail-installer.js +174 -174
  3. package/dist/scaffold.js +68 -68
  4. package/package.json +58 -58
  5. package/sails/_template/addon.json +20 -20
  6. package/sails/_template/install.ts +402 -402
  7. package/sails/admin-dashboard/README.md +117 -117
  8. package/sails/admin-dashboard/addon.json +28 -28
  9. package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -34
  10. package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -243
  11. package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -40
  12. package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -240
  13. package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -149
  14. package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -173
  15. package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -203
  16. package/sails/admin-dashboard/install.ts +305 -305
  17. package/sails/analytics/README.md +178 -178
  18. package/sails/analytics/addon.json +27 -27
  19. package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -58
  20. package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -64
  21. package/sails/analytics/files/frontend/lib/analytics.ts +103 -103
  22. package/sails/analytics/install.ts +297 -297
  23. package/sails/file-uploads/addon.json +30 -30
  24. package/sails/file-uploads/files/backend/routes/files.ts +198 -198
  25. package/sails/file-uploads/files/backend/schema/files.ts +36 -36
  26. package/sails/file-uploads/files/backend/services/file-storage.ts +128 -128
  27. package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -248
  28. package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -147
  29. package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -106
  30. package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -118
  31. package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -37
  32. package/sails/file-uploads/install.ts +466 -466
  33. package/sails/gdpr/README.md +174 -174
  34. package/sails/gdpr/addon.json +27 -27
  35. package/sails/gdpr/files/backend/routes/gdpr.ts +140 -140
  36. package/sails/gdpr/files/backend/services/gdpr.ts +293 -293
  37. package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -97
  38. package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -192
  39. package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -75
  40. package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -186
  41. package/sails/gdpr/install.ts +756 -756
  42. package/sails/google-oauth/README.md +121 -121
  43. package/sails/google-oauth/addon.json +22 -22
  44. package/sails/google-oauth/files/GoogleButton.tsx +50 -50
  45. package/sails/google-oauth/install.ts +252 -252
  46. package/sails/i18n/README.md +193 -193
  47. package/sails/i18n/addon.json +30 -30
  48. package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -108
  49. package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -31
  50. package/sails/i18n/files/frontend/lib/i18n.ts +32 -32
  51. package/sails/i18n/files/frontend/locales/de/common.json +44 -44
  52. package/sails/i18n/files/frontend/locales/en/common.json +44 -44
  53. package/sails/i18n/install.ts +407 -407
  54. package/sails/push-notifications/README.md +163 -163
  55. package/sails/push-notifications/addon.json +31 -31
  56. package/sails/push-notifications/files/backend/routes/notifications.ts +153 -153
  57. package/sails/push-notifications/files/backend/schema/notifications.ts +31 -31
  58. package/sails/push-notifications/files/backend/services/notifications.ts +117 -117
  59. package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -12
  60. package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -154
  61. package/sails/push-notifications/install.ts +384 -384
  62. package/sails/r2-storage/addon.json +29 -29
  63. package/sails/r2-storage/files/backend/services/storage.ts +71 -71
  64. package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -167
  65. package/sails/r2-storage/install.ts +412 -412
  66. package/sails/rate-limiting/addon.json +20 -20
  67. package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -104
  68. package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -137
  69. package/sails/rate-limiting/install.ts +300 -300
  70. package/sails/registry.json +107 -107
  71. package/sails/stripe/README.md +214 -214
  72. package/sails/stripe/addon.json +24 -24
  73. package/sails/stripe/files/backend/routes/stripe.ts +154 -154
  74. package/sails/stripe/files/backend/schema/stripe.ts +74 -74
  75. package/sails/stripe/files/backend/services/stripe.ts +224 -224
  76. package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -135
  77. package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -86
  78. package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -116
  79. package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -226
  80. 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
+ });