create-sprint 0.0.52 → 0.0.56

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.
@@ -9,7 +9,7 @@ export function generateJWTKeys() {
9
9
  }
10
10
  export function getTypeScriptPackageJson(name, telemetry) {
11
11
  const deps = {
12
- "sprint-es": "^0.0.38"
12
+ "sprint-es": "^0.0.48"
13
13
  };
14
14
  const devDeps = {
15
15
  "@types/node": "^22.0.0",
@@ -39,7 +39,7 @@ export function getTypeScriptPackageJson(name, telemetry) {
39
39
  }
40
40
  export function getJavaScriptPackageJson(name, telemetry) {
41
41
  const deps = {
42
- "sprint-es": "^0.0.38"
42
+ "sprint-es": "^0.0.48"
43
43
  };
44
44
  if (telemetry === "sentry" || telemetry === "glitchtip") {
45
45
  deps["@sentry/node"] = "^8.0.0";
@@ -117,13 +117,35 @@ export default defineConfig({
117
117
  export function getMainFile(language) {
118
118
  if (language === "typescript") {
119
119
  return `import Sprint from "sprint-es";
120
+ import homeRouter from "./routes/home";
121
+ import adminRouter from "./routes/admin";
122
+ import authInternalMiddleware from "./middlewares/auth.internal";
123
+ import authUserMiddleware from "./middlewares/auth.user";
120
124
 
121
125
  const app = new Sprint();
126
+
127
+ app.use(authUserMiddleware);
128
+ app.use(authInternalMiddleware);
129
+ app.route("/", homeRouter);
130
+ app.route("/admin", adminRouter);
131
+
132
+ app.listen();
122
133
  `;
123
134
  }
124
135
  return `import Sprint from "sprint-es";
136
+ import homeRouter from "./routes/home.js";
137
+ import adminRouter from "./routes/admin.js";
138
+ import authInternalMiddleware from "./middlewares/auth.internal.js";
139
+ import authUserMiddleware from "./middlewares/auth.user.js";
125
140
 
126
141
  const app = new Sprint();
142
+
143
+ app.use(authUserMiddleware);
144
+ app.use(authInternalMiddleware);
145
+ app.route("/", homeRouter);
146
+ app.route("/admin", adminRouter);
147
+
148
+ app.listen();
127
149
  `;
128
150
  }
129
151
  export function getHomeRoute(language) {
@@ -135,7 +157,7 @@ import { homeController, jwtValidateController } from "@/controllers/home";
135
157
  const router = Router();
136
158
 
137
159
  router.get("/", homeSchema, homeController);
138
- router.post("/jwt/validate", jwtValidateController);
160
+ router.post("/me", jwtValidateController);
139
161
 
140
162
  export default router;
141
163
  `;
@@ -147,7 +169,7 @@ import { homeController, jwtValidateController } from "../controllers/home.js";
147
169
  const router = Router();
148
170
 
149
171
  router.get("/", homeSchema, homeController);
150
- router.post("/jwt/validate", jwtValidateController);
172
+ router.post("/me", jwtValidateController);
151
173
 
152
174
  export default router;
153
175
  `;
@@ -193,24 +215,7 @@ export const homeController: Handler = (req: SprintRequest, res: SprintResponse)
193
215
  };
194
216
 
195
217
  export const jwtValidateController: Handler = (req: SprintRequest, res: SprintResponse) => {
196
- const token = req.sprint?.getAuthorization() || req.headers.authorization;
197
-
198
- if (!token) {
199
- return res.status(401).json({ error: "No token provided" });
200
- }
201
-
202
- try {
203
- const { publicKey } = getJwtFromEnv();
204
- const decoded = verifyEncrypted(token, publicKey);
205
-
206
- if (!decoded) {
207
- return res.status(401).json({ error: "Invalid token" });
208
- }
209
-
210
- res.json({ valid: true, payload: decoded });
211
- } catch (error) {
212
- return res.status(500).json({ error: "JWT not configured" });
213
- }
218
+ return res.json(req.custom.user);
214
219
  };
215
220
  `;
216
221
  }
@@ -225,24 +230,7 @@ export const homeController = (req: SprintRequest, res: SprintResponse) => {
225
230
  };
226
231
 
227
232
  export const jwtValidateController = (req: SprintRequest, res: SprintResponse) => {
228
- const token = req.sprint?.getAuthorization() || req.headers.authorization;
229
-
230
- if (!token) {
231
- return res.status(401).json({ error: "No token provided" });
232
- }
233
-
234
- try {
235
- const { publicKey } = getJwtFromEnv();
236
- const decoded = verifyEncrypted(token, publicKey);
237
-
238
- if (!decoded) {
239
- return res.status(401).json({ error: "Invalid token" });
240
- }
241
-
242
- res.json({ valid: true, payload: decoded });
243
- } catch (error) {
244
- return res.status(500).json({ error: "JWT not configured" });
245
- }
233
+ return res.json(req.custom.user);
246
234
  };
247
235
  `;
248
236
  }
@@ -271,9 +259,9 @@ export const jwtGenerateController: Handler = (req: SprintRequest, res: SprintRe
271
259
  const { userId, role } = req.body || {};
272
260
 
273
261
  try {
274
- const { privateKey } = getJwtFromEnv();
262
+ const { privateKey, encryptionSecret } = getJwtFromEnv();
275
263
  const payload = { userId, role: role || "user" };
276
- const token = signEncrypted(payload, privateKey, { expiresIn: "1h" });
264
+ const token = signEncrypted(payload, privateKey, encryptionSecret, { expiresIn: "1h" });
277
265
  res.json({ token });
278
266
  } catch (error) {
279
267
  return res.status(500).json({ error: "JWT not configured" });
@@ -281,7 +269,8 @@ export const jwtGenerateController: Handler = (req: SprintRequest, res: SprintRe
281
269
  };
282
270
  `;
283
271
  }
284
- return `import { Handler, SprintRequest, SprintResponse } from "sprint-es";
272
+ else {
273
+ return `import { Handler, SprintRequest, SprintResponse } from "sprint-es";
285
274
  import { signEncrypted, getJwtFromEnv } from "sprint-es/jwt";
286
275
 
287
276
  export const adminController = (req: SprintRequest, res: SprintResponse) => {
@@ -304,15 +293,16 @@ export const jwtGenerateController = (req: SprintRequest, res: SprintResponse) =
304
293
  const { userId, role } = req.body || {};
305
294
 
306
295
  try {
307
- const { privateKey } = getJwtFromEnv();
296
+ const { privateKey, encryptionSecret } = getJwtFromEnv();
308
297
  const payload = { userId, role: role || "user" };
309
- const token = signEncrypted(payload, privateKey, { expiresIn: "1h" });
298
+ const token = signEncrypted(payload, privateKey, encryptionSecret, { expiresIn: "1h" });
310
299
  res.json({ token });
311
300
  } catch (error) {
312
301
  return res.status(500).json({ error: "JWT not configured" });
313
302
  }
314
303
  };
315
304
  `;
305
+ }
316
306
  }
317
307
  export function getHomeSchema(language) {
318
308
  if (language === "typescript") {
@@ -368,26 +358,21 @@ export const jwtGenerateSchema = defineRouteSchema({
368
358
  });
369
359
  `;
370
360
  }
371
- export function getAuthMiddleware(language) {
361
+ export function getInternalAuthMiddleware(language) {
372
362
  if (language === "typescript") {
373
363
  return `import { defineMiddleware } from "sprint-es";
374
364
 
375
365
  export default defineMiddleware({
376
- name: "auth",
366
+ name: "adminAuth",
377
367
  priority: 10,
378
368
  include: "/admin/**",
379
369
  handler: (req, res, next) => {
380
370
  const auth = req.sprint.getAuthorization();
381
-
382
- if (!auth) {
383
- return res.status(401).json({ error: "No authorization header" });
384
- }
371
+ if (!auth) return res.status(401).json({ error: "No authorization header" });
385
372
 
386
373
  const token = auth.replace("Bearer ", "");
387
374
 
388
- if (token !== "admin-token") {
389
- return res.status(403).json({ error: "Invalid token" });
390
- }
375
+ if (token !== "admin-token") return res.status(403).json({ error: "Invalid token" });
391
376
 
392
377
  next();
393
378
  }
@@ -397,21 +382,72 @@ export default defineMiddleware({
397
382
  return `import { defineMiddleware } from "sprint-es";
398
383
 
399
384
  export default defineMiddleware({
400
- name: "auth",
385
+ name: "adminAuth",
401
386
  priority: 10,
402
387
  include: "/admin/**",
403
388
  handler: (req, res, next) => {
404
389
  const auth = req.sprint.getAuthorization();
390
+ if (!auth) return res.status(401).json({ error: "No authorization header" });
391
+
392
+ const token = auth.replace("Bearer ", "");
405
393
 
406
- if (!auth) {
407
- return res.status(401).json({ error: "No authorization header" });
408
- }
394
+ if (token !== "admin-token") return res.status(403).json({ error: "Invalid token" });
395
+
396
+ next();
397
+ }
398
+ });
399
+ `;
400
+ }
401
+ export function getUserAuthMiddleware(language) {
402
+ if (language === "typescript") {
403
+ return `import { defineMiddleware } from "sprint-es";
404
+ import { verifyEncrypted, getJwtFromEnv } from "sprint-es/jwt";
405
+
406
+ const { publicKey, encryptionSecret } = getJwtFromEnv();
407
+
408
+ export default defineMiddleware({
409
+ name: "userAuth",
410
+ priority: 10,
411
+ include: "/**",
412
+ exclude: "/admin/**",
413
+ handler: (req, res, next) => {
414
+ const auth = req.sprint.getAuthorization();
415
+ if (!auth) return res.status(401).json({ error: "No authorization header" });
409
416
 
410
417
  const token = auth.replace("Bearer ", "");
411
418
 
412
- if (token !== "admin-token") {
413
- return res.status(403).json({ error: "Invalid token" });
414
- }
419
+ const decoded = verifyEncrypted(token, publicKey, encryptionSecret);
420
+
421
+ if (!decoded) return res.status(403).json({ error: "Invalid token" });
422
+
423
+ req.custom.user = decoded;
424
+
425
+ next();
426
+ }
427
+ });
428
+ `;
429
+ }
430
+ return `import { defineMiddleware } from "sprint-es";
431
+ import { verifyEncrypted, getJwtFromEnv } from "sprint-es/jwt";
432
+
433
+ const { publicKey, encryptionSecret } = getJwtFromEnv();
434
+
435
+ export default defineMiddleware({
436
+ name: "userAuth",
437
+ priority: 10,
438
+ include: "/**",
439
+ exclude: "/admin/**",
440
+ handler: (req, res, next) => {
441
+ const auth = req.sprint.getAuthorization();
442
+ if (!auth) return res.status(401).json({ error: "No authorization header" });
443
+
444
+ const token = auth.replace("Bearer ", "");
445
+
446
+ const decoded = verifyEncrypted(token, publicKey, encryptionSecret);
447
+
448
+ if (!decoded) return res.status(403).json({ error: "Invalid token" });
449
+
450
+ req.custom.user = decoded;
415
451
 
416
452
  next();
417
453
  }
@@ -453,8 +489,7 @@ CMD ["npm", "start"]
453
489
  `;
454
490
  }
455
491
  export function getDockerCompose(language) {
456
- return `version: "3.8"
457
-
492
+ return `
458
493
  services:
459
494
  app:
460
495
  build: .
@@ -462,7 +497,7 @@ services:
462
497
  - "3000:3000"
463
498
  environment:
464
499
  - NODE_ENV=production
465
- - PORT=3000
500
+ - PORT=5000
466
501
  restart: unless-stopped
467
502
  `;
468
503
  }
@@ -554,7 +589,7 @@ initTelemetry({
554
589
 
555
590
  initTelemetry({
556
591
  provider: "discord",
557
- webhookUrl: process.env.DISCORD_WEBHOOK_URL || ""
592
+ webhookUrl: process.env.DISCORD_TELEMETRY_WEBHOOK_URL || ""
558
593
  });
559
594
  `;
560
595
  }
@@ -581,7 +616,7 @@ import { initTelemetry } from "sprint-es/telemetry";
581
616
 
582
617
  initTelemetry({
583
618
  provider: "discord",
584
- webhookUrl: process.env.DISCORD_WEBHOOK_URL || ""
619
+ webhookUrl: process.env.DISCORD_TELEMETRY_WEBHOOK_URL || ""
585
620
  });
586
621
  `;
587
622
  }
@@ -602,7 +637,7 @@ SENTRY_DSN=
602
637
  else if (telemetry === "discord") {
603
638
  env += `
604
639
  # Discord Webhook URL for error notifications
605
- DISCORD_WEBHOOK_URL=
640
+ DISCORD_TELEMETRY_WEBHOOK_URL=
606
641
  `;
607
642
  }
608
643
  return env;
@@ -627,7 +662,7 @@ SENTRY_DSN=
627
662
  else if (telemetry === "discord") {
628
663
  env += `
629
664
  # Discord Webhook URL
630
- DISCORD_WEBHOOK_URL=
665
+ DISCORD_TELEMETRY_WEBHOOK_URL=
631
666
  `;
632
667
  }
633
668
  return env;
@@ -649,7 +684,7 @@ SENTRY_DSN=
649
684
  else if (telemetry === "discord") {
650
685
  env += `
651
686
  # Discord Webhook URL
652
- DISCORD_WEBHOOK_URL=
687
+ DISCORD_TELEMETRY_WEBHOOK_URL=
653
688
  `;
654
689
  }
655
690
  return env;
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { join } from "path";
5
5
  import color from "picocolors";
6
6
  import * as p from "@clack/prompts";
7
7
  import { validateProjectName } from "./validators.js";
8
- import { getTypeScriptPackageJson, getJavaScriptPackageJson, getTsConfig, getViteConfig, getMainFile, getHomeRoute, getAdminRoute, getHomeController, getAdminController, getAuthMiddleware, getHomeSchema, getAdminSchema, getDockerfile, getDockerCompose, getGitignore, getDockerIgnore, getSprintConfigFile, getEnvDevelopment, getEnvProduction, getExampleCronJob } from "./generators.js";
8
+ import { getTypeScriptPackageJson, getJavaScriptPackageJson, getTsConfig, getViteConfig, getMainFile, getHomeRoute, getAdminRoute, getHomeController, getAdminController, getInternalAuthMiddleware, getUserAuthMiddleware, getHomeSchema, getAdminSchema, getDockerfile, getDockerCompose, getGitignore, getDockerIgnore, getSprintConfigFile, getEnvDevelopment, getEnvProduction, getExampleCronJob } from "./generators.js";
9
9
  export async function writeFile(path, content, options) {
10
10
  if (typeof content === "string")
11
11
  content = content.trimEnd();
@@ -16,41 +16,58 @@ export async function runCLI(args) {
16
16
  const options = parseArgs(args);
17
17
  p.intro("Sprint — Quickly API Framework");
18
18
  p.intro(`${color.bgCyan(color.black(' create-sprint-app '))}`);
19
- const config = await p.group({
20
- projectName: () => p.text({
21
- message: "Project name:",
22
- placeholder: "my-api",
23
- validate: (v) => validateProjectName(v) || undefined,
24
- }),
25
- language: () => p.select({
26
- message: "Language:",
27
- options: [
28
- { value: "typescript", label: "TypeScript", hint: "recommended" },
29
- { value: "javascript", label: "JavaScript" },
30
- ],
31
- }),
32
- telemetry: () => p.select({
33
- message: "Error tracking:",
34
- options: [
35
- { value: "none", label: "None" },
36
- { value: "sentry", label: "Sentry", hint: "free tier available" },
37
- { value: "glitchtip", label: "GlitchTip", hint: "self-hostable" },
38
- { value: "discord", label: "Discord Webhook", hint: "sends to a channel" },
39
- ],
40
- }),
41
- docker: () => p.confirm({ message: "Add Docker support?", initialValue: false }),
42
- }, {
43
- onCancel: () => {
44
- p.cancel("Cancelled.");
45
- process.exit(0);
46
- },
47
- });
19
+ let config;
20
+ if (options.skipPrompts) {
21
+ config = {
22
+ projectName: options.projectName || "sprint-app",
23
+ language: options.language || "typescript",
24
+ telemetry: options.telemetry || "none",
25
+ docker: options.docker || false,
26
+ };
27
+ }
28
+ else {
29
+ config = await p.group({
30
+ projectName: () => p.text({
31
+ message: "Project name:",
32
+ placeholder: "my-api",
33
+ validate: (v) => validateProjectName(v) || undefined,
34
+ }),
35
+ language: () => p.select({
36
+ message: "Language:",
37
+ options: [
38
+ { value: "typescript", label: "TypeScript", hint: "recommended" },
39
+ { value: "javascript", label: "JavaScript" },
40
+ ],
41
+ }),
42
+ telemetry: () => p.select({
43
+ message: "Error tracking:",
44
+ options: [
45
+ { value: "none", label: "None" },
46
+ { value: "sentry", label: "Sentry", hint: "free tier available" },
47
+ { value: "glitchtip", label: "GlitchTip", hint: "self-hostable" },
48
+ { value: "discord", label: "Discord Webhook", hint: "sends to a channel" },
49
+ ],
50
+ }),
51
+ docker: () => p.confirm({ message: "Add Docker support?", initialValue: false }),
52
+ }, {
53
+ onCancel: () => {
54
+ p.cancel("Cancelled.");
55
+ process.exit(0);
56
+ },
57
+ });
58
+ }
48
59
  const targetDir = config.projectName === "." ? process.cwd() : join(process.cwd(), config.projectName);
49
60
  const s = p.spinner();
50
61
  s.start("Creating project");
51
62
  await createProject(config.projectName, config.language, config.telemetry, config.docker);
52
63
  s.stop("Project created");
53
- const installDeps = await p.confirm({ message: "Install dependencies now?", initialValue: true });
64
+ let installDeps = true;
65
+ if (options.skipInstall) {
66
+ installDeps = false;
67
+ }
68
+ else if (!options.skipPrompts) {
69
+ installDeps = await p.confirm({ message: "Install dependencies now?", initialValue: true });
70
+ }
54
71
  const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
55
72
  if (installDeps) {
56
73
  const s2 = p.spinner();
@@ -59,7 +76,7 @@ export async function runCLI(args) {
59
76
  await new Promise((resolve, reject) => {
60
77
  const child = spawn(npmCmd, ["install"], {
61
78
  cwd: targetDir,
62
- stdio: "pipe"
79
+ stdio: "inherit"
63
80
  });
64
81
  child.on("close", (code) => {
65
82
  if (code === 0)
@@ -67,12 +84,16 @@ export async function runCLI(args) {
67
84
  else
68
85
  reject(new Error(`npm install exited with code ${code}`));
69
86
  });
70
- child.on("error", reject);
87
+ child.on("error", (err) => {
88
+ p.cancel(`Failed to run npm install: ${err.message}`);
89
+ reject(err);
90
+ });
71
91
  });
72
92
  s2.stop("Dependencies installed");
73
93
  }
74
- catch {
94
+ catch (err) {
75
95
  s2.stop("Install failed — run npm install manually");
96
+ console.error(err);
76
97
  }
77
98
  }
78
99
  const cdCmd = config.projectName === "." ? "" : `cd ${config.projectName} && `;
@@ -158,7 +179,8 @@ async function createProject(projectName, language, telemetry, useDocker) {
158
179
  await writeFile(join(srcDir, "routes", "admin." + (language === "typescript" ? "ts" : "js")), getAdminRoute(language));
159
180
  await writeFile(join(srcDir, "controllers", "home." + (language === "typescript" ? "ts" : "js")), getHomeController(language));
160
181
  await writeFile(join(srcDir, "controllers", "admin." + (language === "typescript" ? "ts" : "js")), getAdminController(language));
161
- await writeFile(join(srcDir, "middlewares", "auth." + (language === "typescript" ? "ts" : "js")), getAuthMiddleware(language));
182
+ await writeFile(join(srcDir, "middlewares", "auth.internal." + (language === "typescript" ? "ts" : "js")), getInternalAuthMiddleware(language));
183
+ await writeFile(join(srcDir, "middlewares", "auth.user." + (language === "typescript" ? "ts" : "js")), getUserAuthMiddleware(language));
162
184
  await writeFile(join(srcDir, "schemas", "home." + (language === "typescript" ? "ts" : "js")), getHomeSchema(language));
163
185
  await writeFile(join(srcDir, "schemas", "admin." + (language === "typescript" ? "ts" : "js")), getAdminSchema(language));
164
186
  await writeFile(join(srcDir, "cronjobs", "example." + (language === "typescript" ? "ts" : "js")), getExampleCronJob(language));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-sprint",
3
- "version": "0.0.52",
3
+ "version": "0.0.56",
4
4
  "description": "Create a new Sprint API project",
5
5
  "type": "module",
6
6
  "bin": {
package/src/generators.ts CHANGED
@@ -16,7 +16,7 @@ export function generateJWTKeys(): JWTKeys {
16
16
 
17
17
  export function getTypeScriptPackageJson(name: string, telemetry: string) {
18
18
  const deps: Record<string, string> = {
19
- "sprint-es": "^0.0.38"
19
+ "sprint-es": "^0.0.48"
20
20
  };
21
21
 
22
22
  const devDeps: Record<string, string> = {
@@ -49,7 +49,7 @@ export function getTypeScriptPackageJson(name: string, telemetry: string) {
49
49
 
50
50
  export function getJavaScriptPackageJson(name: string, telemetry: string) {
51
51
  const deps: Record<string, string> = {
52
- "sprint-es": "^0.0.38"
52
+ "sprint-es": "^0.0.48"
53
53
  };
54
54
 
55
55
  if (telemetry === "sentry" || telemetry === "glitchtip") {
@@ -131,14 +131,36 @@ export default defineConfig({
131
131
  export function getMainFile(language: string) {
132
132
  if (language === "typescript") {
133
133
  return `import Sprint from "sprint-es";
134
+ import homeRouter from "./routes/home";
135
+ import adminRouter from "./routes/admin";
136
+ import authInternalMiddleware from "./middlewares/auth.internal";
137
+ import authUserMiddleware from "./middlewares/auth.user";
134
138
 
135
139
  const app = new Sprint();
140
+
141
+ app.use(authUserMiddleware);
142
+ app.use(authInternalMiddleware);
143
+ app.route("/", homeRouter);
144
+ app.route("/admin", adminRouter);
145
+
146
+ app.listen();
136
147
  `;
137
148
  }
138
149
 
139
150
  return `import Sprint from "sprint-es";
151
+ import homeRouter from "./routes/home.js";
152
+ import adminRouter from "./routes/admin.js";
153
+ import authInternalMiddleware from "./middlewares/auth.internal.js";
154
+ import authUserMiddleware from "./middlewares/auth.user.js";
140
155
 
141
156
  const app = new Sprint();
157
+
158
+ app.use(authUserMiddleware);
159
+ app.use(authInternalMiddleware);
160
+ app.route("/", homeRouter);
161
+ app.route("/admin", adminRouter);
162
+
163
+ app.listen();
142
164
  `;
143
165
  }
144
166
 
@@ -151,7 +173,7 @@ import { homeController, jwtValidateController } from "@/controllers/home";
151
173
  const router = Router();
152
174
 
153
175
  router.get("/", homeSchema, homeController);
154
- router.post("/jwt/validate", jwtValidateController);
176
+ router.post("/me", jwtValidateController);
155
177
 
156
178
  export default router;
157
179
  `;
@@ -163,7 +185,7 @@ import { homeController, jwtValidateController } from "../controllers/home.js";
163
185
  const router = Router();
164
186
 
165
187
  router.get("/", homeSchema, homeController);
166
- router.post("/jwt/validate", jwtValidateController);
188
+ router.post("/me", jwtValidateController);
167
189
 
168
190
  export default router;
169
191
  `;
@@ -211,24 +233,7 @@ export const homeController: Handler = (req: SprintRequest, res: SprintResponse)
211
233
  };
212
234
 
213
235
  export const jwtValidateController: Handler = (req: SprintRequest, res: SprintResponse) => {
214
- const token = req.sprint?.getAuthorization() || req.headers.authorization;
215
-
216
- if (!token) {
217
- return res.status(401).json({ error: "No token provided" });
218
- }
219
-
220
- try {
221
- const { publicKey } = getJwtFromEnv();
222
- const decoded = verifyEncrypted(token, publicKey);
223
-
224
- if (!decoded) {
225
- return res.status(401).json({ error: "Invalid token" });
226
- }
227
-
228
- res.json({ valid: true, payload: decoded });
229
- } catch (error) {
230
- return res.status(500).json({ error: "JWT not configured" });
231
- }
236
+ return res.json(req.custom.user);
232
237
  };
233
238
  `;
234
239
  }
@@ -243,24 +248,7 @@ export const homeController = (req: SprintRequest, res: SprintResponse) => {
243
248
  };
244
249
 
245
250
  export const jwtValidateController = (req: SprintRequest, res: SprintResponse) => {
246
- const token = req.sprint?.getAuthorization() || req.headers.authorization;
247
-
248
- if (!token) {
249
- return res.status(401).json({ error: "No token provided" });
250
- }
251
-
252
- try {
253
- const { publicKey } = getJwtFromEnv();
254
- const decoded = verifyEncrypted(token, publicKey);
255
-
256
- if (!decoded) {
257
- return res.status(401).json({ error: "Invalid token" });
258
- }
259
-
260
- res.json({ valid: true, payload: decoded });
261
- } catch (error) {
262
- return res.status(500).json({ error: "JWT not configured" });
263
- }
251
+ return res.json(req.custom.user);
264
252
  };
265
253
  `;
266
254
  }
@@ -290,17 +278,17 @@ export const jwtGenerateController: Handler = (req: SprintRequest, res: SprintRe
290
278
  const { userId, role } = req.body || {};
291
279
 
292
280
  try {
293
- const { privateKey } = getJwtFromEnv();
281
+ const { privateKey, encryptionSecret } = getJwtFromEnv();
294
282
  const payload = { userId, role: role || "user" };
295
- const token = signEncrypted(payload, privateKey, { expiresIn: "1h" });
283
+ const token = signEncrypted(payload, privateKey, encryptionSecret, { expiresIn: "1h" });
296
284
  res.json({ token });
297
285
  } catch (error) {
298
286
  return res.status(500).json({ error: "JWT not configured" });
299
287
  }
300
288
  };
301
289
  `;
302
- }
303
- return `import { Handler, SprintRequest, SprintResponse } from "sprint-es";
290
+ } else {
291
+ return `import { Handler, SprintRequest, SprintResponse } from "sprint-es";
304
292
  import { signEncrypted, getJwtFromEnv } from "sprint-es/jwt";
305
293
 
306
294
  export const adminController = (req: SprintRequest, res: SprintResponse) => {
@@ -323,16 +311,16 @@ export const jwtGenerateController = (req: SprintRequest, res: SprintResponse) =
323
311
  const { userId, role } = req.body || {};
324
312
 
325
313
  try {
326
- const { privateKey } = getJwtFromEnv();
314
+ const { privateKey, encryptionSecret } = getJwtFromEnv();
327
315
  const payload = { userId, role: role || "user" };
328
- const token = signEncrypted(payload, privateKey, { expiresIn: "1h" });
316
+ const token = signEncrypted(payload, privateKey, encryptionSecret, { expiresIn: "1h" });
329
317
  res.json({ token });
330
318
  } catch (error) {
331
319
  return res.status(500).json({ error: "JWT not configured" });
332
320
  }
333
321
  };
334
322
  `;
335
-
323
+ }
336
324
  }
337
325
 
338
326
  export function getHomeSchema(language: string) {
@@ -391,26 +379,21 @@ export const jwtGenerateSchema = defineRouteSchema({
391
379
  `;
392
380
  }
393
381
 
394
- export function getAuthMiddleware(language: string) {
382
+ export function getInternalAuthMiddleware(language: string) {
395
383
  if (language === "typescript") {
396
384
  return `import { defineMiddleware } from "sprint-es";
397
385
 
398
386
  export default defineMiddleware({
399
- name: "auth",
387
+ name: "adminAuth",
400
388
  priority: 10,
401
389
  include: "/admin/**",
402
390
  handler: (req, res, next) => {
403
391
  const auth = req.sprint.getAuthorization();
404
-
405
- if (!auth) {
406
- return res.status(401).json({ error: "No authorization header" });
407
- }
392
+ if (!auth) return res.status(401).json({ error: "No authorization header" });
408
393
 
409
394
  const token = auth.replace("Bearer ", "");
410
395
 
411
- if (token !== "admin-token") {
412
- return res.status(403).json({ error: "Invalid token" });
413
- }
396
+ if (token !== "admin-token") return res.status(403).json({ error: "Invalid token" });
414
397
 
415
398
  next();
416
399
  }
@@ -420,21 +403,73 @@ export default defineMiddleware({
420
403
  return `import { defineMiddleware } from "sprint-es";
421
404
 
422
405
  export default defineMiddleware({
423
- name: "auth",
406
+ name: "adminAuth",
424
407
  priority: 10,
425
408
  include: "/admin/**",
426
409
  handler: (req, res, next) => {
427
410
  const auth = req.sprint.getAuthorization();
411
+ if (!auth) return res.status(401).json({ error: "No authorization header" });
412
+
413
+ const token = auth.replace("Bearer ", "");
428
414
 
429
- if (!auth) {
430
- return res.status(401).json({ error: "No authorization header" });
431
- }
415
+ if (token !== "admin-token") return res.status(403).json({ error: "Invalid token" });
416
+
417
+ next();
418
+ }
419
+ });
420
+ `;
421
+ }
422
+
423
+ export function getUserAuthMiddleware(language: string) {
424
+ if (language === "typescript") {
425
+ return `import { defineMiddleware } from "sprint-es";
426
+ import { verifyEncrypted, getJwtFromEnv } from "sprint-es/jwt";
427
+
428
+ const { publicKey, encryptionSecret } = getJwtFromEnv();
429
+
430
+ export default defineMiddleware({
431
+ name: "userAuth",
432
+ priority: 10,
433
+ include: "/**",
434
+ exclude: "/admin/**",
435
+ handler: (req, res, next) => {
436
+ const auth = req.sprint.getAuthorization();
437
+ if (!auth) return res.status(401).json({ error: "No authorization header" });
432
438
 
433
439
  const token = auth.replace("Bearer ", "");
434
440
 
435
- if (token !== "admin-token") {
436
- return res.status(403).json({ error: "Invalid token" });
437
- }
441
+ const decoded = verifyEncrypted(token, publicKey, encryptionSecret);
442
+
443
+ if (!decoded) return res.status(403).json({ error: "Invalid token" });
444
+
445
+ req.custom.user = decoded;
446
+
447
+ next();
448
+ }
449
+ });
450
+ `;
451
+ }
452
+ return `import { defineMiddleware } from "sprint-es";
453
+ import { verifyEncrypted, getJwtFromEnv } from "sprint-es/jwt";
454
+
455
+ const { publicKey, encryptionSecret } = getJwtFromEnv();
456
+
457
+ export default defineMiddleware({
458
+ name: "userAuth",
459
+ priority: 10,
460
+ include: "/**",
461
+ exclude: "/admin/**",
462
+ handler: (req, res, next) => {
463
+ const auth = req.sprint.getAuthorization();
464
+ if (!auth) return res.status(401).json({ error: "No authorization header" });
465
+
466
+ const token = auth.replace("Bearer ", "");
467
+
468
+ const decoded = verifyEncrypted(token, publicKey, encryptionSecret);
469
+
470
+ if (!decoded) return res.status(403).json({ error: "Invalid token" });
471
+
472
+ req.custom.user = decoded;
438
473
 
439
474
  next();
440
475
  }
@@ -478,8 +513,7 @@ CMD ["npm", "start"]
478
513
  }
479
514
 
480
515
  export function getDockerCompose(language: string) {
481
- return `version: "3.8"
482
-
516
+ return `
483
517
  services:
484
518
  app:
485
519
  build: .
@@ -487,7 +521,7 @@ services:
487
521
  - "3000:3000"
488
522
  environment:
489
523
  - NODE_ENV=production
490
- - PORT=3000
524
+ - PORT=5000
491
525
  restart: unless-stopped
492
526
  `;
493
527
  }
@@ -582,7 +616,7 @@ initTelemetry({
582
616
 
583
617
  initTelemetry({
584
618
  provider: "discord",
585
- webhookUrl: process.env.DISCORD_WEBHOOK_URL || ""
619
+ webhookUrl: process.env.DISCORD_TELEMETRY_WEBHOOK_URL || ""
586
620
  });
587
621
  `;
588
622
  }
@@ -611,7 +645,7 @@ import { initTelemetry } from "sprint-es/telemetry";
611
645
 
612
646
  initTelemetry({
613
647
  provider: "discord",
614
- webhookUrl: process.env.DISCORD_WEBHOOK_URL || ""
648
+ webhookUrl: process.env.DISCORD_TELEMETRY_WEBHOOK_URL || ""
615
649
  });
616
650
  `;
617
651
  }
@@ -634,7 +668,7 @@ SENTRY_DSN=
634
668
  } else if (telemetry === "discord") {
635
669
  env += `
636
670
  # Discord Webhook URL for error notifications
637
- DISCORD_WEBHOOK_URL=
671
+ DISCORD_TELEMETRY_WEBHOOK_URL=
638
672
  `;
639
673
  }
640
674
 
@@ -662,7 +696,7 @@ SENTRY_DSN=
662
696
  } else if (telemetry === "discord") {
663
697
  env += `
664
698
  # Discord Webhook URL
665
- DISCORD_WEBHOOK_URL=
699
+ DISCORD_TELEMETRY_WEBHOOK_URL=
666
700
  `;
667
701
  }
668
702
 
@@ -686,7 +720,7 @@ SENTRY_DSN=
686
720
  } else if (telemetry === "discord") {
687
721
  env += `
688
722
  # Discord Webhook URL
689
- DISCORD_WEBHOOK_URL=
723
+ DISCORD_TELEMETRY_WEBHOOK_URL=
690
724
  `;
691
725
  }
692
726
 
package/src/index.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import { spawn } from "child_process";
2
- import { existsSync, writeFileSync } from "fs";
2
+ import { existsSync } from "fs";
3
3
  import { mkdir, writeFile as fsWriteFile } from "fs/promises";
4
4
  import { join } from "path";
5
5
  import color from "picocolors";
6
6
  import * as p from "@clack/prompts";
7
7
  import { validateProjectName } from "./validators.js";
8
- import { getTypeScriptPackageJson, getJavaScriptPackageJson, getTsConfig, getViteConfig, getMainFile, getHomeRoute, getAdminRoute, getHomeController, getAdminController, getAuthMiddleware, getHomeSchema, getAdminSchema, getDockerfile, getDockerCompose, getGitignore, getDockerIgnore, getSprintConfigFile, getEnvDevelopment, getEnvProduction, getExampleCronJob } from "./generators.js";
8
+ import { getTypeScriptPackageJson, getJavaScriptPackageJson, getTsConfig, getViteConfig, getMainFile, getHomeRoute, getAdminRoute, getHomeController, getAdminController, getInternalAuthMiddleware, getUserAuthMiddleware, getHomeSchema, getAdminSchema, getDockerfile, getDockerCompose, getGitignore, getDockerIgnore, getSprintConfigFile, getEnvDevelopment, getEnvProduction, getExampleCronJob } from "./generators.js";
9
9
 
10
10
  export interface CLIOptions {
11
11
  projectName?: string;
@@ -28,59 +28,81 @@ export async function runCLI(args: string[]) {
28
28
 
29
29
  p.intro(`${color.bgCyan(color.black(' create-sprint-app '))}`);
30
30
 
31
- const config = await p.group(
32
- {
33
- projectName: () =>
34
- p.text({
35
- message: "Project name:",
36
- placeholder: "my-api",
37
- validate: (v) => validateProjectName(v) || undefined,
38
- }),
39
-
40
- language: () =>
41
- p.select({
42
- message: "Language:",
43
- options: [
44
- { value: "typescript", label: "TypeScript", hint: "recommended" },
45
- { value: "javascript", label: "JavaScript" },
46
- ],
47
- }),
48
-
49
- telemetry: () =>
50
- p.select({
51
- message: "Error tracking:",
52
- options: [
53
- { value: "none", label: "None" },
54
- { value: "sentry", label: "Sentry", hint: "free tier available" },
55
- { value: "glitchtip", label: "GlitchTip", hint: "self-hostable" },
56
- { value: "discord", label: "Discord Webhook", hint: "sends to a channel" },
57
- ],
58
- }),
59
-
60
- docker: () =>
61
- p.confirm({ message: "Add Docker support?", initialValue: false }),
62
- },
63
- {
64
- onCancel: () => {
65
- p.cancel("Cancelled.");
66
- process.exit(0);
31
+ let config: {
32
+ projectName: string;
33
+ language: "typescript" | "javascript";
34
+ telemetry: string;
35
+ docker: boolean;
36
+ };
37
+
38
+ if (options.skipPrompts) {
39
+ config = {
40
+ projectName: options.projectName || "sprint-app",
41
+ language: options.language || "typescript",
42
+ telemetry: options.telemetry || "none",
43
+ docker: options.docker || false,
44
+ };
45
+ } else {
46
+ config = await p.group(
47
+ {
48
+ projectName: () =>
49
+ p.text({
50
+ message: "Project name:",
51
+ placeholder: "my-api",
52
+ validate: (v) => validateProjectName(v) || undefined,
53
+ }),
54
+
55
+ language: () =>
56
+ p.select({
57
+ message: "Language:",
58
+ options: [
59
+ { value: "typescript", label: "TypeScript", hint: "recommended" },
60
+ { value: "javascript", label: "JavaScript" },
61
+ ],
62
+ }),
63
+
64
+ telemetry: () =>
65
+ p.select({
66
+ message: "Error tracking:",
67
+ options: [
68
+ { value: "none", label: "None" },
69
+ { value: "sentry", label: "Sentry", hint: "free tier available" },
70
+ { value: "glitchtip", label: "GlitchTip", hint: "self-hostable" },
71
+ { value: "discord", label: "Discord Webhook", hint: "sends to a channel" },
72
+ ],
73
+ }),
74
+
75
+ docker: () =>
76
+ p.confirm({ message: "Add Docker support?", initialValue: false }),
67
77
  },
68
- }
69
- );
78
+ {
79
+ onCancel: () => {
80
+ p.cancel("Cancelled.");
81
+ process.exit(0);
82
+ },
83
+ }
84
+ );
85
+ }
70
86
 
71
87
  const targetDir = config.projectName === "." ? process.cwd() : join(process.cwd(), config.projectName);
72
88
 
73
89
  const s = p.spinner();
74
90
  s.start("Creating project");
75
91
  await createProject(
76
- config.projectName as string,
77
- config.language as "typescript" | "javascript",
78
- config.telemetry as string,
79
- config.docker as boolean,
92
+ config.projectName,
93
+ config.language,
94
+ config.telemetry,
95
+ config.docker,
80
96
  );
81
97
  s.stop("Project created");
82
98
 
83
- const installDeps = await p.confirm({ message: "Install dependencies now?", initialValue: true });
99
+ let installDeps = true;
100
+ if (options.skipInstall) {
101
+ installDeps = false;
102
+ } else if (!options.skipPrompts) {
103
+ installDeps = await p.confirm({ message: "Install dependencies now?", initialValue: true }) as boolean;
104
+ }
105
+
84
106
  const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
85
107
 
86
108
  if (installDeps) {
@@ -90,17 +112,21 @@ export async function runCLI(args: string[]) {
90
112
  await new Promise<void>((resolve, reject) => {
91
113
  const child = spawn(npmCmd, ["install"], {
92
114
  cwd: targetDir,
93
- stdio: "pipe"
115
+ stdio: "inherit"
94
116
  });
95
117
  child.on("close", (code) => {
96
118
  if (code === 0) resolve();
97
119
  else reject(new Error(`npm install exited with code ${code}`));
98
120
  });
99
- child.on("error", reject);
121
+ child.on("error", (err) => {
122
+ p.cancel(`Failed to run npm install: ${err.message}`);
123
+ reject(err);
124
+ });
100
125
  });
101
126
  s2.stop("Dependencies installed");
102
- } catch {
127
+ } catch (err) {
103
128
  s2.stop("Install failed — run npm install manually");
129
+ console.error(err);
104
130
  }
105
131
  }
106
132
 
@@ -202,7 +228,8 @@ async function createProject(
202
228
  await writeFile(join(srcDir, "controllers", "home." + (language === "typescript" ? "ts" : "js")), getHomeController(language));
203
229
  await writeFile(join(srcDir, "controllers", "admin." + (language === "typescript" ? "ts" : "js")), getAdminController(language));
204
230
 
205
- await writeFile(join(srcDir, "middlewares", "auth." + (language === "typescript" ? "ts" : "js")), getAuthMiddleware(language));
231
+ await writeFile(join(srcDir, "middlewares", "auth.internal." + (language === "typescript" ? "ts" : "js")), getInternalAuthMiddleware(language));
232
+ await writeFile(join(srcDir, "middlewares", "auth.user." + (language === "typescript" ? "ts" : "js")), getUserAuthMiddleware(language));
206
233
 
207
234
  await writeFile(join(srcDir, "schemas", "home." + (language === "typescript" ? "ts" : "js")), getHomeSchema(language));
208
235
  await writeFile(join(srcDir, "schemas", "admin." + (language === "typescript" ? "ts" : "js")), getAdminSchema(language));