@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.
- package/dist/__tests__/cli.test.d.ts +2 -0
- package/dist/__tests__/cli.test.d.ts.map +1 -0
- package/dist/__tests__/cli.test.js +173 -0
- package/dist/__tests__/cli.test.js.map +1 -0
- package/dist/__tests__/registry.test.d.ts +2 -0
- package/dist/__tests__/registry.test.d.ts.map +1 -0
- package/dist/__tests__/registry.test.js +86 -0
- package/dist/__tests__/registry.test.js.map +1 -0
- package/dist/__tests__/sail-installer.test.d.ts +2 -0
- package/dist/__tests__/sail-installer.test.d.ts.map +1 -0
- package/dist/__tests__/sail-installer.test.js +158 -0
- package/dist/__tests__/sail-installer.test.js.map +1 -0
- package/dist/create-runner.d.ts +11 -0
- package/dist/create-runner.d.ts.map +1 -0
- package/dist/create-runner.js +63 -0
- package/dist/create-runner.js.map +1 -0
- package/dist/create.d.ts +10 -0
- package/dist/create.d.ts.map +1 -0
- package/dist/create.js +15 -0
- package/dist/create.js.map +1 -0
- package/dist/manage.d.ts +24 -0
- package/dist/manage.d.ts.map +1 -0
- package/dist/manage.js +1461 -0
- package/dist/manage.js.map +1 -0
- package/dist/prompts.d.ts +36 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +208 -0
- package/dist/prompts.js.map +1 -0
- package/dist/sail-installer.d.ts +37 -0
- package/dist/sail-installer.d.ts.map +1 -0
- package/dist/sail-installer.js +935 -0
- package/dist/sail-installer.js.map +1 -0
- package/dist/scaffold.d.ts +10 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +297 -0
- package/dist/scaffold.js.map +1 -0
- package/package.json +57 -0
- package/sails/_template/addon.json +20 -0
- package/sails/_template/install.ts +402 -0
- package/sails/admin-dashboard/README.md +117 -0
- package/sails/admin-dashboard/addon.json +28 -0
- package/sails/admin-dashboard/files/backend/middleware/admin.ts +34 -0
- package/sails/admin-dashboard/files/backend/routes/admin.ts +243 -0
- package/sails/admin-dashboard/files/frontend/components/admin/StatsCard.tsx +40 -0
- package/sails/admin-dashboard/files/frontend/components/admin/UsersTable.tsx +240 -0
- package/sails/admin-dashboard/files/frontend/hooks/useAdmin.ts +149 -0
- package/sails/admin-dashboard/files/frontend/pages/admin/Dashboard.tsx +173 -0
- package/sails/admin-dashboard/files/frontend/pages/admin/UserDetail.tsx +203 -0
- package/sails/admin-dashboard/install.ts +305 -0
- package/sails/analytics/README.md +178 -0
- package/sails/analytics/addon.json +27 -0
- package/sails/analytics/files/frontend/components/AnalyticsProvider.tsx +58 -0
- package/sails/analytics/files/frontend/hooks/useAnalytics.ts +64 -0
- package/sails/analytics/files/frontend/lib/analytics.ts +103 -0
- package/sails/analytics/install.ts +297 -0
- package/sails/file-uploads/README.md +191 -0
- package/sails/file-uploads/addon.json +30 -0
- package/sails/file-uploads/files/backend/routes/files.ts +198 -0
- package/sails/file-uploads/files/backend/schema/files.ts +36 -0
- package/sails/file-uploads/files/backend/services/file-storage.ts +128 -0
- package/sails/file-uploads/files/frontend/components/FileList.tsx +248 -0
- package/sails/file-uploads/files/frontend/components/FileUploadButton.tsx +147 -0
- package/sails/file-uploads/files/frontend/hooks/useFileUpload.ts +106 -0
- package/sails/file-uploads/files/frontend/hooks/useFiles.ts +118 -0
- package/sails/file-uploads/files/frontend/pages/Files.tsx +37 -0
- package/sails/file-uploads/install.ts +466 -0
- package/sails/gdpr/README.md +174 -0
- package/sails/gdpr/addon.json +27 -0
- package/sails/gdpr/files/backend/routes/gdpr.ts +140 -0
- package/sails/gdpr/files/backend/services/gdpr.ts +293 -0
- package/sails/gdpr/files/frontend/components/auth/ConsentCheckboxes.tsx +97 -0
- package/sails/gdpr/files/frontend/components/gdpr/AccountDeletionRequest.tsx +192 -0
- package/sails/gdpr/files/frontend/components/gdpr/DataExportButton.tsx +75 -0
- package/sails/gdpr/files/frontend/pages/PrivacyPolicy.tsx +186 -0
- package/sails/gdpr/install.ts +756 -0
- package/sails/google-oauth/README.md +121 -0
- package/sails/google-oauth/addon.json +22 -0
- package/sails/google-oauth/files/GoogleButton.tsx +50 -0
- package/sails/google-oauth/install.ts +252 -0
- package/sails/i18n/README.md +193 -0
- package/sails/i18n/addon.json +30 -0
- package/sails/i18n/files/frontend/components/LanguageSwitcher.tsx +108 -0
- package/sails/i18n/files/frontend/hooks/useLanguage.ts +31 -0
- package/sails/i18n/files/frontend/lib/i18n.ts +32 -0
- package/sails/i18n/files/frontend/locales/de/common.json +44 -0
- package/sails/i18n/files/frontend/locales/en/common.json +44 -0
- package/sails/i18n/install.ts +407 -0
- package/sails/push-notifications/README.md +163 -0
- package/sails/push-notifications/addon.json +31 -0
- package/sails/push-notifications/files/backend/routes/notifications.ts +153 -0
- package/sails/push-notifications/files/backend/schema/notifications.ts +31 -0
- package/sails/push-notifications/files/backend/services/notifications.ts +117 -0
- package/sails/push-notifications/files/frontend/components/PushNotificationInit.tsx +12 -0
- package/sails/push-notifications/files/frontend/hooks/usePushNotifications.ts +154 -0
- package/sails/push-notifications/install.ts +384 -0
- package/sails/r2-storage/README.md +101 -0
- package/sails/r2-storage/addon.json +29 -0
- package/sails/r2-storage/files/backend/services/storage.ts +71 -0
- package/sails/r2-storage/files/frontend/components/ProfilePictureUpload.tsx +167 -0
- package/sails/r2-storage/install.ts +412 -0
- package/sails/rate-limiting/README.md +145 -0
- package/sails/rate-limiting/addon.json +20 -0
- package/sails/rate-limiting/files/backend/middleware/rate-limit-store.ts +104 -0
- package/sails/rate-limiting/files/backend/middleware/rate-limit.ts +137 -0
- package/sails/rate-limiting/install.ts +300 -0
- package/sails/registry.json +107 -0
- package/sails/stripe/README.md +214 -0
- package/sails/stripe/addon.json +24 -0
- package/sails/stripe/files/backend/routes/stripe.ts +154 -0
- package/sails/stripe/files/backend/schema/stripe.ts +74 -0
- package/sails/stripe/files/backend/services/stripe.ts +224 -0
- package/sails/stripe/files/frontend/components/SubscriptionStatus.tsx +135 -0
- package/sails/stripe/files/frontend/hooks/useSubscription.ts +86 -0
- package/sails/stripe/files/frontend/pages/Checkout.tsx +116 -0
- package/sails/stripe/files/frontend/pages/Pricing.tsx +226 -0
- package/sails/stripe/install.ts +378 -0
|
@@ -0,0 +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
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.0.0",
|
|
3
|
+
"sails": [
|
|
4
|
+
{
|
|
5
|
+
"name": "google-oauth",
|
|
6
|
+
"displayName": "Google OAuth",
|
|
7
|
+
"description": "Google OAuth provider for BetterAuth — adds Google sign-in button",
|
|
8
|
+
"category": "auth",
|
|
9
|
+
"version": "1.0.0",
|
|
10
|
+
"routes": [],
|
|
11
|
+
"envVars": ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET"],
|
|
12
|
+
"conflicts": []
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "stripe",
|
|
16
|
+
"displayName": "Stripe Payments",
|
|
17
|
+
"description": "Stripe subscriptions with checkout, webhooks, and customer portal",
|
|
18
|
+
"category": "payments",
|
|
19
|
+
"version": "1.0.0",
|
|
20
|
+
"routes": ["/api/stripe"],
|
|
21
|
+
"envVars": ["STRIPE_SECRET_KEY", "STRIPE_PUBLISHABLE_KEY", "STRIPE_WEBHOOK_SECRET"],
|
|
22
|
+
"conflicts": []
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "r2-storage",
|
|
26
|
+
"displayName": "Cloudflare R2 Storage",
|
|
27
|
+
"description": "File uploads via Cloudflare R2 with presigned URLs. Adds profile picture upload.",
|
|
28
|
+
"category": "storage",
|
|
29
|
+
"version": "1.0.0",
|
|
30
|
+
"routes": [],
|
|
31
|
+
"envVars": ["R2_ACCOUNT_ID", "R2_ACCESS_KEY_ID", "R2_SECRET_ACCESS_KEY", "R2_BUCKET_NAME", "R2_PUBLIC_URL"],
|
|
32
|
+
"conflicts": ["file-uploads"]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "push-notifications",
|
|
36
|
+
"displayName": "Push Notifications",
|
|
37
|
+
"description": "Push notifications via Capacitor + Firebase Cloud Messaging with device token management and server-side sending",
|
|
38
|
+
"category": "mobile",
|
|
39
|
+
"version": "1.0.0",
|
|
40
|
+
"routes": ["/api/notifications"],
|
|
41
|
+
"envVars": ["FIREBASE_PROJECT_ID", "FIREBASE_PRIVATE_KEY", "FIREBASE_CLIENT_EMAIL"],
|
|
42
|
+
"conflicts": []
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "analytics",
|
|
46
|
+
"displayName": "PostHog Analytics",
|
|
47
|
+
"description": "Privacy-friendly analytics with PostHog — auto page views, user identification, and custom event tracking",
|
|
48
|
+
"category": "tracking",
|
|
49
|
+
"version": "1.0.0",
|
|
50
|
+
"routes": [],
|
|
51
|
+
"envVars": ["VITE_POSTHOG_KEY", "VITE_POSTHOG_HOST"],
|
|
52
|
+
"conflicts": []
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "admin-dashboard",
|
|
56
|
+
"displayName": "Admin Dashboard",
|
|
57
|
+
"description": "Admin dashboard for user management, metrics, and basic analytics",
|
|
58
|
+
"category": "admin",
|
|
59
|
+
"version": "1.0.0",
|
|
60
|
+
"routes": ["/api/admin"],
|
|
61
|
+
"envVars": ["ADMIN_EMAILS"],
|
|
62
|
+
"conflicts": []
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "i18n",
|
|
66
|
+
"displayName": "Internationalization",
|
|
67
|
+
"description": "Multi-language support with i18next, react-i18next, and language detection",
|
|
68
|
+
"category": "localization",
|
|
69
|
+
"version": "1.0.0",
|
|
70
|
+
"routes": [],
|
|
71
|
+
"envVars": [],
|
|
72
|
+
"conflicts": []
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"name": "rate-limiting",
|
|
76
|
+
"displayName": "Rate Limiting",
|
|
77
|
+
"description": "API rate limiting middleware",
|
|
78
|
+
"category": "security",
|
|
79
|
+
"version": "1.0.0",
|
|
80
|
+
"status": "planned",
|
|
81
|
+
"routes": [],
|
|
82
|
+
"envVars": [],
|
|
83
|
+
"conflicts": []
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "file-uploads",
|
|
87
|
+
"displayName": "File Uploads",
|
|
88
|
+
"description": "Generic file upload system with R2/S3 storage",
|
|
89
|
+
"category": "storage",
|
|
90
|
+
"version": "1.0.0",
|
|
91
|
+
"status": "planned",
|
|
92
|
+
"routes": ["/api/files"],
|
|
93
|
+
"envVars": ["S3_ENDPOINT", "S3_ACCESS_KEY_ID", "S3_SECRET_ACCESS_KEY", "S3_BUCKET_NAME", "S3_PUBLIC_URL", "S3_REGION"],
|
|
94
|
+
"conflicts": ["r2-storage"]
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"name": "gdpr",
|
|
98
|
+
"displayName": "GDPR/DSGVO Compliance",
|
|
99
|
+
"description": "Full GDPR compliance: consent tracking, data export, account deletion (30-day grace period), privacy policy page",
|
|
100
|
+
"category": "compliance",
|
|
101
|
+
"version": "1.0.0",
|
|
102
|
+
"routes": ["/api/gdpr"],
|
|
103
|
+
"envVars": ["DELETION_CRON_SECRET"],
|
|
104
|
+
"conflicts": []
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Stripe Payments Sail
|
|
2
|
+
|
|
3
|
+
Adds subscription management to your keel application using Stripe Checkout, webhooks, and the Customer Portal.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Stripe Checkout for subscription payments
|
|
8
|
+
- Webhook handling for subscription lifecycle events
|
|
9
|
+
- Customer Portal for self-service subscription management
|
|
10
|
+
- Pricing page with plan cards
|
|
11
|
+
- Subscription status component for dashboards and settings
|
|
12
|
+
- Drizzle ORM schema for customers and subscriptions
|
|
13
|
+
|
|
14
|
+
## Prerequisites
|
|
15
|
+
|
|
16
|
+
- A Stripe account (https://stripe.com)
|
|
17
|
+
- Stripe CLI (for local webhook testing)
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx tsx sails/stripe/install.ts
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The installer will prompt for your Stripe API keys and configure everything automatically.
|
|
26
|
+
|
|
27
|
+
## Manual Setup: Stripe Dashboard
|
|
28
|
+
|
|
29
|
+
### 1. Get API Keys
|
|
30
|
+
|
|
31
|
+
1. Go to https://dashboard.stripe.com/test/apikeys
|
|
32
|
+
2. Copy the **Publishable key** (pk_test_...)
|
|
33
|
+
3. Copy the **Secret key** (sk_test_...)
|
|
34
|
+
4. Add both to your `.env` file
|
|
35
|
+
|
|
36
|
+
### 2. Create Products and Prices
|
|
37
|
+
|
|
38
|
+
1. Go to https://dashboard.stripe.com/test/products
|
|
39
|
+
2. Click **Add product**
|
|
40
|
+
3. Create your subscription plans (e.g., "Pro" and "Enterprise")
|
|
41
|
+
4. For each product, add a **Recurring price** (e.g., $19/month)
|
|
42
|
+
5. Copy the Price IDs (price_...) and update `src/pages/Pricing.tsx`
|
|
43
|
+
|
|
44
|
+
### 3. Set Up Webhooks
|
|
45
|
+
|
|
46
|
+
#### Development (using Stripe CLI)
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Install Stripe CLI: https://stripe.com/docs/stripe-cli
|
|
50
|
+
stripe login
|
|
51
|
+
stripe listen --forward-to localhost:3000/api/stripe/webhook
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The CLI will output a webhook signing secret (whsec_...). Add it to your `.env` as `STRIPE_WEBHOOK_SECRET`.
|
|
55
|
+
|
|
56
|
+
#### Production
|
|
57
|
+
|
|
58
|
+
1. Go to https://dashboard.stripe.com/webhooks
|
|
59
|
+
2. Click **Add endpoint**
|
|
60
|
+
3. Set the URL to `https://yourdomain.com/api/stripe/webhook`
|
|
61
|
+
4. Select events:
|
|
62
|
+
- `checkout.session.completed`
|
|
63
|
+
- `customer.subscription.updated`
|
|
64
|
+
- `customer.subscription.deleted`
|
|
65
|
+
5. Click **Add endpoint**
|
|
66
|
+
6. Copy the **Signing secret** and set it as `STRIPE_WEBHOOK_SECRET`
|
|
67
|
+
|
|
68
|
+
### 4. Configure Customer Portal
|
|
69
|
+
|
|
70
|
+
1. Go to https://dashboard.stripe.com/test/settings/billing/portal
|
|
71
|
+
2. Enable the features you want (cancel, update payment method, etc.)
|
|
72
|
+
3. Save changes
|
|
73
|
+
|
|
74
|
+
### 5. Environment Variables
|
|
75
|
+
|
|
76
|
+
```env
|
|
77
|
+
STRIPE_SECRET_KEY=sk_test_...
|
|
78
|
+
STRIPE_PUBLISHABLE_KEY=pk_test_...
|
|
79
|
+
STRIPE_WEBHOOK_SECRET=whsec_...
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Architecture
|
|
83
|
+
|
|
84
|
+
### Database Schema
|
|
85
|
+
|
|
86
|
+
**stripe_customers**
|
|
87
|
+
| Column | Type | Description |
|
|
88
|
+
|--------|------|-------------|
|
|
89
|
+
| id | uuid | Primary key |
|
|
90
|
+
| user_id | text | FK to users table |
|
|
91
|
+
| stripe_customer_id | text | Stripe Customer ID |
|
|
92
|
+
| created_at | timestamp | Creation time |
|
|
93
|
+
| updated_at | timestamp | Last update time |
|
|
94
|
+
|
|
95
|
+
**stripe_subscriptions**
|
|
96
|
+
| Column | Type | Description |
|
|
97
|
+
|--------|------|-------------|
|
|
98
|
+
| id | uuid | Primary key |
|
|
99
|
+
| customer_id | uuid | FK to stripe_customers |
|
|
100
|
+
| stripe_subscription_id | text | Stripe Subscription ID |
|
|
101
|
+
| status | text | active, trialing, past_due, canceled, etc. |
|
|
102
|
+
| stripe_price_id | text | The Stripe Price ID |
|
|
103
|
+
| current_period_start | timestamp | Current billing period start |
|
|
104
|
+
| current_period_end | timestamp | Current billing period end |
|
|
105
|
+
| cancel_at_period_end | boolean | Whether sub cancels at period end |
|
|
106
|
+
| created_at | timestamp | Creation time |
|
|
107
|
+
| updated_at | timestamp | Last update time |
|
|
108
|
+
|
|
109
|
+
### API Routes
|
|
110
|
+
|
|
111
|
+
| Method | Path | Auth | Description |
|
|
112
|
+
|--------|------|------|-------------|
|
|
113
|
+
| POST | /api/stripe/create-checkout-session | Yes | Create a Checkout session |
|
|
114
|
+
| POST | /api/stripe/create-portal-session | Yes | Create a Customer Portal session |
|
|
115
|
+
| POST | /api/stripe/webhook | No | Stripe webhook handler |
|
|
116
|
+
| GET | /api/stripe/subscription | Yes | Get current subscription |
|
|
117
|
+
|
|
118
|
+
### Webhook Flow
|
|
119
|
+
|
|
120
|
+
1. Stripe fires an event (e.g., `checkout.session.completed`)
|
|
121
|
+
2. Express receives the raw body at `/api/stripe/webhook`
|
|
122
|
+
3. Signature is verified using `STRIPE_WEBHOOK_SECRET`
|
|
123
|
+
4. The event handler updates the database accordingly
|
|
124
|
+
5. Returns 200 to acknowledge receipt
|
|
125
|
+
|
|
126
|
+
### Important: Raw Body Middleware
|
|
127
|
+
|
|
128
|
+
The webhook endpoint requires the raw request body for signature verification. Make sure your Express app has this middleware **before** `express.json()`:
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
app.use("/api/stripe/webhook", express.raw({ type: "application/json" }));
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The installer adds this automatically.
|
|
135
|
+
|
|
136
|
+
## Frontend Components
|
|
137
|
+
|
|
138
|
+
### Pricing Page (`/pricing`)
|
|
139
|
+
|
|
140
|
+
Displays plan cards with features and subscribe buttons. Redirects to Stripe Checkout when a plan is selected.
|
|
141
|
+
|
|
142
|
+
### Checkout Page (`/checkout/success`, `/checkout/cancel`)
|
|
143
|
+
|
|
144
|
+
Post-checkout landing pages for successful payments and cancellations.
|
|
145
|
+
|
|
146
|
+
### SubscriptionStatus Component
|
|
147
|
+
|
|
148
|
+
Drop-in component showing the current subscription status with a "Manage" button that opens the Stripe Customer Portal. Use it in settings or dashboard pages:
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
import { SubscriptionStatus } from "@/components/stripe/SubscriptionStatus";
|
|
152
|
+
|
|
153
|
+
function SettingsPage() {
|
|
154
|
+
return (
|
|
155
|
+
<div>
|
|
156
|
+
<h2>Subscription</h2>
|
|
157
|
+
<SubscriptionStatus />
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### useSubscription Hook
|
|
164
|
+
|
|
165
|
+
```tsx
|
|
166
|
+
import { useSubscription, isSubscriptionActive } from "@/hooks/useSubscription";
|
|
167
|
+
|
|
168
|
+
function MyComponent() {
|
|
169
|
+
const { subscription, isLoading } = useSubscription();
|
|
170
|
+
|
|
171
|
+
if (isSubscriptionActive(subscription)) {
|
|
172
|
+
return <PremiumContent />;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return <UpgradePrompt />;
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Testing
|
|
180
|
+
|
|
181
|
+
### Test Cards
|
|
182
|
+
|
|
183
|
+
Use Stripe's test card numbers:
|
|
184
|
+
|
|
185
|
+
| Card | Number | Scenario |
|
|
186
|
+
|------|--------|----------|
|
|
187
|
+
| Visa | 4242 4242 4242 4242 | Successful payment |
|
|
188
|
+
| Visa (declined) | 4000 0000 0000 0002 | Card declined |
|
|
189
|
+
| Visa (3D Secure) | 4000 0025 0000 3155 | Requires authentication |
|
|
190
|
+
|
|
191
|
+
Use any future expiry date, any 3-digit CVC, and any ZIP code.
|
|
192
|
+
|
|
193
|
+
### Testing Webhooks Locally
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# Terminal 1: Start your dev server
|
|
197
|
+
npm run dev
|
|
198
|
+
|
|
199
|
+
# Terminal 2: Forward Stripe events
|
|
200
|
+
stripe listen --forward-to localhost:3000/api/stripe/webhook
|
|
201
|
+
|
|
202
|
+
# Terminal 3: Trigger test events
|
|
203
|
+
stripe trigger checkout.session.completed
|
|
204
|
+
stripe trigger customer.subscription.updated
|
|
205
|
+
stripe trigger customer.subscription.deleted
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Going to Production
|
|
209
|
+
|
|
210
|
+
1. Switch from test keys to live keys in your environment
|
|
211
|
+
2. Create live products/prices and update Price IDs
|
|
212
|
+
3. Set up the production webhook endpoint
|
|
213
|
+
4. Configure the Customer Portal for live mode
|
|
214
|
+
5. Test the full flow with a real card
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "stripe",
|
|
3
|
+
"displayName": "Stripe Payments",
|
|
4
|
+
"description": "Adds Stripe subscription management with checkout, webhooks, and customer portal",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"compatibility": ">=1.0.0",
|
|
7
|
+
"requiredEnvVars": [
|
|
8
|
+
{ "key": "STRIPE_SECRET_KEY", "description": "Stripe secret key (sk_test_... or sk_live_...)" },
|
|
9
|
+
{ "key": "STRIPE_PUBLISHABLE_KEY", "description": "Stripe publishable key (pk_test_... or pk_live_...)" },
|
|
10
|
+
{ "key": "STRIPE_WEBHOOK_SECRET", "description": "Stripe webhook signing secret (whsec_...)" }
|
|
11
|
+
],
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"backend": { "stripe": "^17.0.0" },
|
|
14
|
+
"frontend": { "@stripe/react-stripe-js": "^3.0.0", "@stripe/stripe-js": "^5.0.0" }
|
|
15
|
+
},
|
|
16
|
+
"modifies": {
|
|
17
|
+
"backend": ["src/index.ts", "src/db/schema/index.ts", "src/env.ts"],
|
|
18
|
+
"frontend": ["src/router.tsx"]
|
|
19
|
+
},
|
|
20
|
+
"adds": {
|
|
21
|
+
"backend": ["src/db/schema/stripe.ts", "src/routes/stripe.ts", "src/services/stripe.ts"],
|
|
22
|
+
"frontend": ["src/pages/Pricing.tsx", "src/pages/Checkout.tsx", "src/components/stripe/SubscriptionStatus.tsx", "src/hooks/useSubscription.ts"]
|
|
23
|
+
}
|
|
24
|
+
}
|