4bnode 4.0.9 → 4.1.1

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 (45) hide show
  1. package/README.md +25 -206
  2. package/add-crud.js +6 -106
  3. package/add-docs.js +91 -0
  4. package/add-email.js +63 -0
  5. package/add-security.js +143 -0
  6. package/index.js +76 -8
  7. package/lib/codegen.js +1223 -0
  8. package/lib/codegen.test.js +480 -0
  9. package/lib/indexFile.js +10 -12
  10. package/lib/names.js +4 -21
  11. package/lib/routeFile.js +9 -27
  12. package/lib/sync-skeleton.js +47 -0
  13. package/lib/ui.js +15 -2
  14. package/package.json +4 -2
  15. package/scripts/audit-dev-surface.mjs +153 -0
  16. package/scripts/reveal-passkey.mjs +35 -0
  17. package/skeleton/.4bnode/assets/js/App.js +31 -9
  18. package/skeleton/.4bnode/assets/js/Root.js +1 -5
  19. package/skeleton/.4bnode/assets/js/components/Sidebar.js +9 -5
  20. package/skeleton/.4bnode/assets/js/lib.js +18 -5
  21. package/skeleton/.4bnode/assets/js/pages/AiBuilderPage.js +268 -0
  22. package/skeleton/.4bnode/assets/js/pages/ApiKeysPage.js +164 -0
  23. package/skeleton/.4bnode/assets/js/pages/ApiTesterPage.js +275 -9
  24. package/skeleton/.4bnode/assets/js/pages/DatabasePage.js +44 -205
  25. package/skeleton/.4bnode/assets/js/pages/DocsPage.js +142 -0
  26. package/skeleton/.4bnode/assets/js/pages/MailPage.js +134 -0
  27. package/skeleton/.4bnode/assets/js/pages/RoutesPage.js +5 -266
  28. package/skeleton/.4bnode/assets/js/pages/SchemasPage.js +23 -213
  29. package/skeleton/.4bnode/assets/js/pages/SecurityPage.js +74 -0
  30. package/skeleton/.4bnode/assets/js/screens/PasskeyScreen.js +61 -61
  31. package/skeleton/.4bnode/codegen.js +1223 -0
  32. package/skeleton/.4bnode/dev-api.js +634 -728
  33. package/skeleton/.4bnode/ui.html +62 -40
  34. package/skeleton/_gitignore +7 -0
  35. package/skeleton/index.js +41 -3
  36. package/skeleton/package.json +4 -1
  37. package/skeleton/src/middleware/apiKey.js +36 -0
  38. package/skeleton/src/middleware/errorHandler.js +15 -0
  39. package/skeleton/src/middleware/roles.js +14 -0
  40. package/skeleton/src/middleware/validate.js +20 -0
  41. package/skeleton/.4bnode/assets/images/mysql.png +0 -0
  42. package/skeleton/.4bnode/assets/images/postgresql.png +0 -0
  43. package/skeleton/.4bnode/assets/images/sqlite.png +0 -0
  44. package/skeleton/.4bnode/assets/js/pages/ReportsPage.js +0 -667
  45. package/skeleton/.4bnode/assets/js/screens/SplashScreen.js +0 -66
package/index.js CHANGED
@@ -54,21 +54,52 @@ function updatePackageName(destinationPath, projectName) {
54
54
  );
55
55
  }
56
56
 
57
- function hashPasskey(pin) {
58
- return crypto.createHash("sha256").update(pin).digest("hex");
57
+ // Salted scrypt (slow KDF) so a leaked .passkey can't be brute-forced or
58
+ // reversed via rainbow tables. Format: scrypt$<saltHex>$<hashHex>.
59
+ function hashPasskey(passphrase) {
60
+ const salt = crypto.randomBytes(16);
61
+ const hash = crypto.scryptSync(String(passphrase), salt, 64);
62
+ return `scrypt$${salt.toString("hex")}$${hash.toString("hex")}`;
63
+ }
64
+
65
+ // A few obviously-weak passkeys to reject outright.
66
+ const WEAK_PASSKEYS = new Set([
67
+ "password", "passw0rd", "12345678", "123456789", "1234567890",
68
+ "qwertyui", "11111111", "00000000", "iloveyou", "letmein1",
69
+ ]);
70
+
71
+ // True if every consecutive char steps by +1 (ascending, e.g. "123456",
72
+ // "abcdef") or by -1 (descending/reverse, e.g. "654321", "fedcba").
73
+ function isSequential(s) {
74
+ if (s.length < 2) return false;
75
+ let asc = true;
76
+ let desc = true;
77
+ for (let i = 1; i < s.length; i++) {
78
+ const step = s.charCodeAt(i) - s.charCodeAt(i - 1);
79
+ if (step !== 1) asc = false;
80
+ if (step !== -1) desc = false;
81
+ }
82
+ return asc || desc;
83
+ }
84
+
85
+ // Returns an error string for a weak passkey, or null if it's acceptable.
86
+ function passkeyWeakness(v) {
87
+ const s = String(v || "");
88
+ if (s.length < 8) return "Passkey must be at least 8 characters";
89
+ if (WEAK_PASSKEYS.has(s.toLowerCase())) return "That passkey is too common — pick something less guessable";
90
+ if (/^(.)\1+$/.test(s)) return "Don't use a single repeated character (e.g. 11111111)";
91
+ if (isSequential(s)) return "Don't use a sequential or reversed run (e.g. 12345678 or 87654321)";
92
+ return null;
59
93
  }
60
94
 
61
95
  async function promptPasskey() {
62
96
  const passkey = await password({
63
- message: "Set a 6-digit passkey for the dev dashboard:",
97
+ message: "Set a dashboard passkey (min 8 chars, letters + numbers recommended):",
64
98
  mask: "*",
65
- validate: (v) => {
66
- if (!/^\d{6}$/.test(v)) return "Passkey must be exactly 6 digits";
67
- return true;
68
- },
99
+ validate: (v) => passkeyWeakness(v) || true,
69
100
  });
70
101
 
71
- const confirm = await password({
102
+ await password({
72
103
  message: "Confirm passkey:",
73
104
  mask: "*",
74
105
  validate: (v) => {
@@ -85,6 +116,20 @@ function savePasskey(projectPath, passkey) {
85
116
  fs.writeFileSync(path.join(projectPath, ".4bnode", ".passkey"), hash, "utf8");
86
117
  }
87
118
 
119
+ // Seed a unique API key so the baked apiKey middleware works out of the box
120
+ // (never bundle a shared key into the skeleton — each project gets its own).
121
+ function seedApiKey(projectPath) {
122
+ const file = path.join(projectPath, "src", "api-keys.json");
123
+ fs.mkdirSync(path.dirname(file), { recursive: true });
124
+ const key = {
125
+ id: crypto.randomBytes(8).toString("hex"),
126
+ label: "Default",
127
+ key: "sk_" + crypto.randomBytes(24).toString("hex"),
128
+ createdAt: new Date().toISOString(),
129
+ };
130
+ fs.writeFileSync(file, JSON.stringify([key], null, 2) + "\n", "utf8");
131
+ }
132
+
88
133
  async function createSkeletonApp(projectName) {
89
134
  showBanner();
90
135
 
@@ -111,6 +156,7 @@ async function createSkeletonApp(projectName) {
111
156
  console.log();
112
157
  const passkey = await promptPasskey();
113
158
  savePasskey(targetPath, passkey);
159
+ seedApiKey(targetPath);
114
160
  console.log(chalk.hex("#00f260").bold(" ✔ Dashboard passkey set"));
115
161
 
116
162
  const installSpinner = createSpinner("Installing dependencies...");
@@ -163,6 +209,17 @@ const addOptions = [
163
209
  requiresRoutes: true,
164
210
  requiresModels: true,
165
211
  },
212
+ {
213
+ name: "API Docs OpenAPI 3 spec + Swagger UI at /docs",
214
+ value: "docs",
215
+ script: "add-docs.js",
216
+ requiresRoutes: true,
217
+ },
218
+ {
219
+ name: "Security Helmet, rate-limit, RBAC, API keys, validation",
220
+ value: "security",
221
+ script: "add-security.js",
222
+ },
166
223
  {
167
224
  name: "Socket.IO Real-time with Socket.IO",
168
225
  value: "socket",
@@ -178,6 +235,11 @@ const addOptions = [
178
235
  value: "serialport",
179
236
  script: "add-serialport.js",
180
237
  },
238
+ {
239
+ name: "Email Transactional email (Brevo, SendGrid, SMTP…)",
240
+ value: "email",
241
+ script: "add-email.js",
242
+ },
181
243
  ];
182
244
 
183
245
  // ── List ────────────────────────────────────────────
@@ -263,9 +325,12 @@ function showHelp() {
263
325
  console.log(` ${c("add mongo")} MongoDB connection`);
264
326
  console.log(` ${c("add crud")} CRUD routes (create/read/update/delete)`);
265
327
  console.log(` ${c("add login")} JWT authentication & login`);
328
+ console.log(` ${c("add docs")} OpenAPI 3 spec + Swagger UI at /docs`);
329
+ console.log(` ${c("add security")} Helmet, rate-limit, RBAC, API keys, validation`);
266
330
  console.log(` ${c("add socket")} Socket.IO integration`);
267
331
  console.log(` ${c("add websocket")} Native WebSocket support`);
268
332
  console.log(` ${c("add serialport")} Serial port communication`);
333
+ console.log(` ${c("add email")} Transactional email (Brevo, SendGrid, SMTP)`);
269
334
  console.log();
270
335
  console.log(h(" OPTIONS\n"));
271
336
  console.log(` ${c("-h, --help")} Show this help message`);
@@ -394,6 +459,8 @@ async function main() {
394
459
  "add-mongo": "mongo",
395
460
  "add-crud": "crud",
396
461
  "add-login": "login",
462
+ "add-docs": "docs",
463
+ "add-security": "security",
397
464
  "add-mongo-insert": "crud",
398
465
  "add-mongo-update": "crud",
399
466
  "add-mongo-read": "crud",
@@ -401,6 +468,7 @@ async function main() {
401
468
  "add-socket": "socket",
402
469
  "add-websocket": "websocket",
403
470
  "add-serialport": "serialport",
471
+ "add-email": "email",
404
472
  };
405
473
  const mapped = legacyMap[command];
406
474
  if (mapped) {