@codaijs/keel 0.1.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.
Files changed (116) hide show
  1. package/dist/__tests__/cli.test.d.ts +2 -0
  2. package/dist/__tests__/cli.test.d.ts.map +1 -0
  3. package/dist/__tests__/cli.test.js +173 -0
  4. package/dist/__tests__/cli.test.js.map +1 -0
  5. package/dist/__tests__/registry.test.d.ts +2 -0
  6. package/dist/__tests__/registry.test.d.ts.map +1 -0
  7. package/dist/__tests__/registry.test.js +86 -0
  8. package/dist/__tests__/registry.test.js.map +1 -0
  9. package/dist/__tests__/sail-installer.test.d.ts +2 -0
  10. package/dist/__tests__/sail-installer.test.d.ts.map +1 -0
  11. package/dist/__tests__/sail-installer.test.js +158 -0
  12. package/dist/__tests__/sail-installer.test.js.map +1 -0
  13. package/dist/create-runner.d.ts +11 -0
  14. package/dist/create-runner.d.ts.map +1 -0
  15. package/dist/create-runner.js +63 -0
  16. package/dist/create-runner.js.map +1 -0
  17. package/dist/create.d.ts +10 -0
  18. package/dist/create.d.ts.map +1 -0
  19. package/dist/create.js +15 -0
  20. package/dist/create.js.map +1 -0
  21. package/dist/manage.d.ts +24 -0
  22. package/dist/manage.d.ts.map +1 -0
  23. package/dist/manage.js +1461 -0
  24. package/dist/manage.js.map +1 -0
  25. package/dist/prompts.d.ts +36 -0
  26. package/dist/prompts.d.ts.map +1 -0
  27. package/dist/prompts.js +208 -0
  28. package/dist/prompts.js.map +1 -0
  29. package/dist/sail-installer.d.ts +37 -0
  30. package/dist/sail-installer.d.ts.map +1 -0
  31. package/dist/sail-installer.js +935 -0
  32. package/dist/sail-installer.js.map +1 -0
  33. package/dist/scaffold.d.ts +10 -0
  34. package/dist/scaffold.d.ts.map +1 -0
  35. package/dist/scaffold.js +297 -0
  36. package/dist/scaffold.js.map +1 -0
  37. package/package.json +57 -0
  38. package/sails/_template/addon.json +20 -0
  39. package/sails/_template/install.ts +402 -0
  40. package/sails/admin-dashboard/README.md +117 -0
  41. package/sails/admin-dashboard/addon.json +28 -0
  42. package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -0
  43. package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -0
  44. package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -0
  45. package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -0
  46. package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -0
  47. package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -0
  48. package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -0
  49. package/sails/admin-dashboard/install.ts +305 -0
  50. package/sails/analytics/README.md +178 -0
  51. package/sails/analytics/addon.json +27 -0
  52. package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -0
  53. package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -0
  54. package/sails/analytics/files/frontend/lib/analytics.ts +103 -0
  55. package/sails/analytics/install.ts +297 -0
  56. package/sails/file-uploads/README.md +191 -0
  57. package/sails/file-uploads/addon.json +30 -0
  58. package/sails/file-uploads/files/backend/routes/files.ts +198 -0
  59. package/sails/file-uploads/files/backend/schema/files.ts +36 -0
  60. package/sails/file-uploads/files/backend/services/file-storage.ts +128 -0
  61. package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -0
  62. package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -0
  63. package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -0
  64. package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -0
  65. package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -0
  66. package/sails/file-uploads/install.ts +466 -0
  67. package/sails/gdpr/README.md +174 -0
  68. package/sails/gdpr/addon.json +27 -0
  69. package/sails/gdpr/files/backend/routes/gdpr.ts +140 -0
  70. package/sails/gdpr/files/backend/services/gdpr.ts +293 -0
  71. package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -0
  72. package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -0
  73. package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -0
  74. package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -0
  75. package/sails/gdpr/install.ts +756 -0
  76. package/sails/google-oauth/README.md +121 -0
  77. package/sails/google-oauth/addon.json +22 -0
  78. package/sails/google-oauth/files/GoogleButton.tsx +50 -0
  79. package/sails/google-oauth/install.ts +252 -0
  80. package/sails/i18n/README.md +193 -0
  81. package/sails/i18n/addon.json +30 -0
  82. package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -0
  83. package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -0
  84. package/sails/i18n/files/frontend/lib/i18n.ts +32 -0
  85. package/sails/i18n/files/frontend/locales/de/common.json +44 -0
  86. package/sails/i18n/files/frontend/locales/en/common.json +44 -0
  87. package/sails/i18n/install.ts +407 -0
  88. package/sails/push-notifications/README.md +163 -0
  89. package/sails/push-notifications/addon.json +31 -0
  90. package/sails/push-notifications/files/backend/routes/notifications.ts +153 -0
  91. package/sails/push-notifications/files/backend/schema/notifications.ts +31 -0
  92. package/sails/push-notifications/files/backend/services/notifications.ts +117 -0
  93. package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -0
  94. package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -0
  95. package/sails/push-notifications/install.ts +384 -0
  96. package/sails/r2-storage/README.md +101 -0
  97. package/sails/r2-storage/addon.json +29 -0
  98. package/sails/r2-storage/files/backend/services/storage.ts +71 -0
  99. package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -0
  100. package/sails/r2-storage/install.ts +412 -0
  101. package/sails/rate-limiting/README.md +145 -0
  102. package/sails/rate-limiting/addon.json +20 -0
  103. package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -0
  104. package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -0
  105. package/sails/rate-limiting/install.ts +300 -0
  106. package/sails/registry.json +107 -0
  107. package/sails/stripe/README.md +214 -0
  108. package/sails/stripe/addon.json +24 -0
  109. package/sails/stripe/files/backend/routes/stripe.ts +154 -0
  110. package/sails/stripe/files/backend/schema/stripe.ts +74 -0
  111. package/sails/stripe/files/backend/services/stripe.ts +224 -0
  112. package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -0
  113. package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -0
  114. package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -0
  115. package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -0
  116. package/sails/stripe/install.ts +378 -0
@@ -0,0 +1,145 @@
1
+ # API Rate Limiting Sail
2
+
3
+ In-memory sliding window rate limiting middleware for Express API routes. No external dependencies or Redis required.
4
+
5
+ ## What this sail adds
6
+
7
+ ### Backend
8
+ - **`src/middleware/rate-limit.ts`** -- Rate limiting middleware factory with preset configurations:
9
+ - `apiLimiter` -- 100 requests per 15 minutes (general API routes)
10
+ - `authLimiter` -- 10 requests per 15 minutes (login, signup)
11
+ - `strictLimiter` -- 5 requests per 15 minutes (password reset, sensitive operations)
12
+ - `createRateLimiter(options)` -- factory for custom configurations
13
+ - **`src/middleware/rate-limit-store.ts`** -- Store abstraction with in-memory implementation:
14
+ - `RateLimitStore` interface for swapping storage backends
15
+ - `MemoryStore` with automatic cleanup of expired entries
16
+
17
+ ### How it works
18
+
19
+ The middleware uses a **sliding window** algorithm:
20
+
21
+ 1. Each client is identified by their authenticated user ID or IP address
22
+ 2. Requests are counted within a configurable time window
23
+ 3. When the limit is exceeded the server responds with `429 Too Many Requests`
24
+ 4. Standard rate limit headers are set on every response:
25
+ - `X-RateLimit-Limit` -- maximum requests allowed
26
+ - `X-RateLimit-Remaining` -- requests remaining in the current window
27
+ - `X-RateLimit-Reset` -- Unix timestamp when the window resets
28
+ - `Retry-After` -- seconds until the client can retry (only on 429)
29
+
30
+ ### Environment variables (optional)
31
+
32
+ | Variable | Description | Default |
33
+ |----------|-------------|---------|
34
+ | `RATE_LIMIT_WINDOW_MS` | Window duration in milliseconds | `900000` (15 min) |
35
+ | `RATE_LIMIT_MAX_REQUESTS` | Max requests per window | `100` |
36
+
37
+ These environment variables override the defaults for `apiLimiter`. The preset `authLimiter` and `strictLimiter` always use their own hardcoded values.
38
+
39
+ ## Setup
40
+
41
+ ### Run the installer
42
+
43
+ ```bash
44
+ npx tsx cli/sails/rate-limiting/install.ts
45
+ ```
46
+
47
+ Or use the CLI:
48
+
49
+ ```bash
50
+ npx @codaijs/keel sail add rate-limiting
51
+ ```
52
+
53
+ The installer will:
54
+ 1. Copy the middleware files into your backend
55
+ 2. Apply the rate limiter to your chosen routes
56
+ 3. Optionally add environment variables for custom defaults
57
+
58
+ ## Usage
59
+
60
+ ### Apply globally
61
+
62
+ ```ts
63
+ import { apiLimiter } from "./middleware/rate-limit.js";
64
+
65
+ app.use("/api", apiLimiter);
66
+ ```
67
+
68
+ ### Apply to specific routes
69
+
70
+ ```ts
71
+ import { authLimiter, strictLimiter } from "./middleware/rate-limit.js";
72
+
73
+ app.use("/api/auth/login", authLimiter);
74
+ app.use("/api/auth/signup", authLimiter);
75
+ app.use("/api/auth/reset-password", strictLimiter);
76
+ ```
77
+
78
+ ### Create a custom limiter
79
+
80
+ ```ts
81
+ import { createRateLimiter } from "./middleware/rate-limit.js";
82
+
83
+ const uploadLimiter = createRateLimiter({
84
+ windowMs: 60 * 60 * 1000, // 1 hour
85
+ maxRequests: 20,
86
+ message: "Upload limit exceeded. Try again later.",
87
+ });
88
+
89
+ app.use("/api/uploads", uploadLimiter);
90
+ ```
91
+
92
+ ### Custom key generator
93
+
94
+ By default clients are identified by user ID (if authenticated) or IP address. You can provide a custom key generator:
95
+
96
+ ```ts
97
+ const limiter = createRateLimiter({
98
+ keyGenerator: (req) => req.headers["x-api-key"] as string ?? req.ip,
99
+ maxRequests: 1000,
100
+ });
101
+ ```
102
+
103
+ ## Scaling to production
104
+
105
+ The default `MemoryStore` works well for single-process deployments. For multi-process or distributed environments, implement the `RateLimitStore` interface backed by Redis:
106
+
107
+ ```ts
108
+ import type { RateLimitStore, RateLimitEntry } from "./middleware/rate-limit-store.js";
109
+
110
+ class RedisStore implements RateLimitStore {
111
+ constructor(private redis: RedisClient) {}
112
+
113
+ async increment(key: string, windowMs: number): Promise<RateLimitEntry> {
114
+ const rKey = `rl:${key}`;
115
+ const count = await this.redis.incr(rKey);
116
+ if (count === 1) {
117
+ await this.redis.pexpire(rKey, windowMs);
118
+ }
119
+ const ttl = await this.redis.pttl(rKey);
120
+ return { count, resetAt: Date.now() + ttl };
121
+ }
122
+
123
+ async decrement(key: string): Promise<void> {
124
+ await this.redis.decr(`rl:${key}`);
125
+ }
126
+
127
+ async reset(key: string): Promise<void> {
128
+ await this.redis.del(`rl:${key}`);
129
+ }
130
+ }
131
+ ```
132
+
133
+ Then pass it when creating your limiter:
134
+
135
+ ```ts
136
+ const limiter = createRateLimiter({
137
+ store: new RedisStore(redis),
138
+ });
139
+ ```
140
+
141
+ ## Troubleshooting
142
+
143
+ - **All requests are getting 429**: Check that the `maxRequests` value is appropriate for your traffic. Authenticated users are tracked by user ID, so shared IPs (like offices) should not cause issues for logged-in users.
144
+ - **Rate limits not working behind a proxy**: Make sure your Express app trusts the proxy so that `X-Forwarded-For` is parsed correctly: `app.set("trust proxy", 1)`.
145
+ - **Memory growing**: The `MemoryStore` prunes expired entries every 5 minutes. If you have very high traffic, consider switching to a Redis store.
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "rate-limiting",
3
+ "displayName": "API Rate Limiting",
4
+ "description": "In-memory sliding window rate limiting middleware for API routes. No external dependencies required.",
5
+ "version": "1.0.0",
6
+ "compatibility": ">=1.0.0",
7
+ "requiredEnvVars": [],
8
+ "dependencies": {
9
+ "backend": {},
10
+ "frontend": {}
11
+ },
12
+ "modifies": {
13
+ "backend": ["src/index.ts", "src/env.ts"],
14
+ "frontend": []
15
+ },
16
+ "adds": {
17
+ "backend": ["src/middleware/rate-limit.ts", "src/middleware/rate-limit-store.ts"],
18
+ "frontend": []
19
+ }
20
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Rate Limit Store Abstraction
3
+ *
4
+ * Provides an interface and in-memory implementation for rate limit tracking.
5
+ * This abstraction allows swapping to a Redis-backed store in production
6
+ * without changing the middleware logic.
7
+ */
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Interface
11
+ // ---------------------------------------------------------------------------
12
+
13
+ export interface RateLimitEntry {
14
+ /** Number of requests made in the current window. */
15
+ count: number;
16
+ /** Timestamp (ms) when the current window resets. */
17
+ resetAt: number;
18
+ }
19
+
20
+ export interface RateLimitStore {
21
+ /**
22
+ * Increment the request count for `key` within a window of `windowMs`.
23
+ * If the key does not exist or the window has expired a new window is
24
+ * started automatically.
25
+ */
26
+ increment(key: string, windowMs: number): Promise<RateLimitEntry>;
27
+
28
+ /** Decrement the count for `key` (useful for undoing a counted request). */
29
+ decrement(key: string): Promise<void>;
30
+
31
+ /** Reset (delete) the entry for `key`. */
32
+ reset(key: string): Promise<void>;
33
+ }
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // In-memory implementation
37
+ // ---------------------------------------------------------------------------
38
+
39
+ /**
40
+ * Simple in-memory store backed by a `Map`.
41
+ *
42
+ * A periodic cleanup timer prunes expired entries every `cleanupIntervalMs`
43
+ * (default 5 minutes) so the map does not grow unboundedly.
44
+ */
45
+ export class MemoryStore implements RateLimitStore {
46
+ private store = new Map<string, RateLimitEntry>();
47
+ private cleanupTimer: ReturnType<typeof setInterval> | null = null;
48
+
49
+ constructor(cleanupIntervalMs = 5 * 60 * 1000) {
50
+ this.cleanupTimer = setInterval(() => {
51
+ this.prune();
52
+ }, cleanupIntervalMs);
53
+
54
+ // Allow the Node process to exit even if the timer is still active.
55
+ if (this.cleanupTimer && typeof this.cleanupTimer === "object" && "unref" in this.cleanupTimer) {
56
+ this.cleanupTimer.unref();
57
+ }
58
+ }
59
+
60
+ async increment(key: string, windowMs: number): Promise<RateLimitEntry> {
61
+ const now = Date.now();
62
+ const existing = this.store.get(key);
63
+
64
+ if (existing && existing.resetAt > now) {
65
+ // Window still active — increment.
66
+ existing.count += 1;
67
+ return { count: existing.count, resetAt: existing.resetAt };
68
+ }
69
+
70
+ // No entry or window expired — start a fresh window.
71
+ const entry: RateLimitEntry = { count: 1, resetAt: now + windowMs };
72
+ this.store.set(key, entry);
73
+ return { count: 1, resetAt: entry.resetAt };
74
+ }
75
+
76
+ async decrement(key: string): Promise<void> {
77
+ const entry = this.store.get(key);
78
+ if (entry && entry.count > 0) {
79
+ entry.count -= 1;
80
+ }
81
+ }
82
+
83
+ async reset(key: string): Promise<void> {
84
+ this.store.delete(key);
85
+ }
86
+
87
+ /** Remove all expired entries from the map. */
88
+ private prune(): void {
89
+ const now = Date.now();
90
+ for (const [key, entry] of this.store) {
91
+ if (entry.resetAt <= now) {
92
+ this.store.delete(key);
93
+ }
94
+ }
95
+ }
96
+
97
+ /** Stop the cleanup timer (useful for tests / graceful shutdown). */
98
+ destroy(): void {
99
+ if (this.cleanupTimer) {
100
+ clearInterval(this.cleanupTimer);
101
+ this.cleanupTimer = null;
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Sliding-window rate limiting middleware for Express.
3
+ *
4
+ * Uses an in-memory store by default (no Redis required). You can swap in any
5
+ * implementation of `RateLimitStore` for distributed deployments.
6
+ *
7
+ * Usage:
8
+ * import { apiLimiter, authLimiter, createRateLimiter } from "./middleware/rate-limit.js";
9
+ *
10
+ * app.use("/api", apiLimiter); // 100 req / 15 min
11
+ * app.use("/api/auth", authLimiter); // 10 req / 15 min
12
+ *
13
+ * // Custom:
14
+ * app.use("/api/special", createRateLimiter({ windowMs: 60_000, maxRequests: 5 }));
15
+ */
16
+
17
+ import type { Request, Response, NextFunction } from "express";
18
+ import { MemoryStore } from "./rate-limit-store.js";
19
+ import type { RateLimitStore } from "./rate-limit-store.js";
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Types
23
+ // ---------------------------------------------------------------------------
24
+
25
+ export interface RateLimitOptions {
26
+ /** Time window in milliseconds. Default: 15 minutes. */
27
+ windowMs?: number;
28
+ /** Maximum number of requests allowed in the window. Default: 100. */
29
+ maxRequests?: number;
30
+ /** Extract the key used to identify the client. Defaults to IP, or userId when authenticated. */
31
+ keyGenerator?: (req: Request) => string;
32
+ /** Store implementation. Defaults to `MemoryStore`. */
33
+ store?: RateLimitStore;
34
+ /** Custom message returned when the limit is exceeded. */
35
+ message?: string;
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Defaults from environment (optional overrides)
40
+ // ---------------------------------------------------------------------------
41
+
42
+ const DEFAULT_WINDOW_MS = Number(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000;
43
+ const DEFAULT_MAX_REQUESTS = Number(process.env.RATE_LIMIT_MAX_REQUESTS) || 100;
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // Shared store — one MemoryStore instance for the entire process
47
+ // ---------------------------------------------------------------------------
48
+
49
+ const sharedStore = new MemoryStore();
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Key generator
53
+ // ---------------------------------------------------------------------------
54
+
55
+ function defaultKeyGenerator(req: Request): string {
56
+ // Prefer the authenticated user id when available so that rate limits are
57
+ // per-user rather than per-IP for logged-in users.
58
+ const userId = (req as Record<string, unknown>).user
59
+ ? ((req as Record<string, unknown>).user as { id?: string })?.id
60
+ : undefined;
61
+
62
+ if (userId) return `user:${userId}`;
63
+
64
+ // Fall back to IP address.
65
+ const forwarded = req.headers["x-forwarded-for"];
66
+ const ip =
67
+ typeof forwarded === "string"
68
+ ? forwarded.split(",")[0].trim()
69
+ : req.socket.remoteAddress ?? "unknown";
70
+
71
+ return `ip:${ip}`;
72
+ }
73
+
74
+ // ---------------------------------------------------------------------------
75
+ // Factory
76
+ // ---------------------------------------------------------------------------
77
+
78
+ /**
79
+ * Create a rate-limiting middleware with the given options.
80
+ */
81
+ export function createRateLimiter(options: RateLimitOptions = {}) {
82
+ const {
83
+ windowMs = DEFAULT_WINDOW_MS,
84
+ maxRequests = DEFAULT_MAX_REQUESTS,
85
+ keyGenerator = defaultKeyGenerator,
86
+ store = sharedStore,
87
+ message = "Too many requests, please try again later.",
88
+ } = options;
89
+
90
+ return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
91
+ const key = keyGenerator(req);
92
+
93
+ try {
94
+ const { count, resetAt } = await store.increment(key, windowMs);
95
+
96
+ // Always set informational headers.
97
+ res.setHeader("X-RateLimit-Limit", String(maxRequests));
98
+ res.setHeader("X-RateLimit-Remaining", String(Math.max(0, maxRequests - count)));
99
+ res.setHeader("X-RateLimit-Reset", String(Math.ceil(resetAt / 1000)));
100
+
101
+ if (count > maxRequests) {
102
+ const retryAfterSeconds = Math.ceil((resetAt - Date.now()) / 1000);
103
+ res.setHeader("Retry-After", String(retryAfterSeconds));
104
+ res.status(429).json({ error: message });
105
+ return;
106
+ }
107
+
108
+ next();
109
+ } catch (err) {
110
+ // If the store fails we let the request through rather than blocking.
111
+ console.error("[rate-limit] Store error:", err);
112
+ next();
113
+ }
114
+ };
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // Preset limiters
119
+ // ---------------------------------------------------------------------------
120
+
121
+ /** General API limiter — 100 requests per 15 minutes. */
122
+ export const apiLimiter = createRateLimiter({
123
+ windowMs: DEFAULT_WINDOW_MS,
124
+ maxRequests: DEFAULT_MAX_REQUESTS,
125
+ });
126
+
127
+ /** Auth limiter — 10 requests per 15 minutes (login, signup). */
128
+ export const authLimiter = createRateLimiter({
129
+ windowMs: 15 * 60 * 1000,
130
+ maxRequests: 10,
131
+ });
132
+
133
+ /** Strict limiter — 5 requests per 15 minutes (password reset, sensitive ops). */
134
+ export const strictLimiter = createRateLimiter({
135
+ windowMs: 15 * 60 * 1000,
136
+ maxRequests: 5,
137
+ });