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.
- package/README.md +25 -206
- package/add-crud.js +6 -106
- package/add-docs.js +91 -0
- package/add-email.js +63 -0
- package/add-security.js +143 -0
- package/index.js +76 -8
- package/lib/codegen.js +1223 -0
- package/lib/codegen.test.js +480 -0
- package/lib/indexFile.js +10 -12
- package/lib/names.js +4 -21
- package/lib/routeFile.js +9 -27
- package/lib/sync-skeleton.js +47 -0
- package/lib/ui.js +15 -2
- package/package.json +4 -2
- package/scripts/audit-dev-surface.mjs +153 -0
- package/scripts/reveal-passkey.mjs +35 -0
- package/skeleton/.4bnode/assets/js/App.js +31 -9
- package/skeleton/.4bnode/assets/js/Root.js +1 -5
- package/skeleton/.4bnode/assets/js/components/Sidebar.js +9 -5
- package/skeleton/.4bnode/assets/js/lib.js +18 -5
- package/skeleton/.4bnode/assets/js/pages/AiBuilderPage.js +268 -0
- package/skeleton/.4bnode/assets/js/pages/ApiKeysPage.js +164 -0
- package/skeleton/.4bnode/assets/js/pages/ApiTesterPage.js +275 -9
- package/skeleton/.4bnode/assets/js/pages/DatabasePage.js +44 -205
- package/skeleton/.4bnode/assets/js/pages/DocsPage.js +142 -0
- package/skeleton/.4bnode/assets/js/pages/MailPage.js +134 -0
- package/skeleton/.4bnode/assets/js/pages/RoutesPage.js +5 -266
- package/skeleton/.4bnode/assets/js/pages/SchemasPage.js +23 -213
- package/skeleton/.4bnode/assets/js/pages/SecurityPage.js +74 -0
- package/skeleton/.4bnode/assets/js/screens/PasskeyScreen.js +61 -61
- package/skeleton/.4bnode/codegen.js +1223 -0
- package/skeleton/.4bnode/dev-api.js +634 -728
- package/skeleton/.4bnode/ui.html +62 -40
- package/skeleton/_gitignore +7 -0
- package/skeleton/index.js +41 -3
- package/skeleton/package.json +4 -1
- package/skeleton/src/middleware/apiKey.js +36 -0
- package/skeleton/src/middleware/errorHandler.js +15 -0
- package/skeleton/src/middleware/roles.js +14 -0
- package/skeleton/src/middleware/validate.js +20 -0
- package/skeleton/.4bnode/assets/images/mysql.png +0 -0
- package/skeleton/.4bnode/assets/images/postgresql.png +0 -0
- package/skeleton/.4bnode/assets/images/sqlite.png +0 -0
- package/skeleton/.4bnode/assets/js/pages/ReportsPage.js +0 -667
- 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
|
-
|
|
58
|
-
|
|
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
|
|
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
|
-
|
|
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) {
|