4bnode 4.1.0 → 4.1.2

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 (46) 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/ecosystem.config.cjs +25 -0
  36. package/skeleton/index.js +46 -5
  37. package/skeleton/package.json +4 -1
  38. package/skeleton/src/middleware/apiKey.js +36 -0
  39. package/skeleton/src/middleware/errorHandler.js +15 -0
  40. package/skeleton/src/middleware/roles.js +14 -0
  41. package/skeleton/src/middleware/validate.js +20 -0
  42. package/skeleton/.4bnode/assets/images/mysql.png +0 -0
  43. package/skeleton/.4bnode/assets/images/postgresql.png +0 -0
  44. package/skeleton/.4bnode/assets/images/sqlite.png +0 -0
  45. package/skeleton/.4bnode/assets/js/pages/ReportsPage.js +0 -667
  46. package/skeleton/.4bnode/assets/js/screens/SplashScreen.js +0 -66
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  ╚═╝╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚══════╝
10
10
  ```
11
11
 
12
- A professional CLI tool and visual dashboard to generate, manage, and test Node.js/Express applications.
12
+ A CLI to generate, manage, and test Node.js/Express applications.
13
13
 
14
14
  Built by [4Brains Technologies](https://4brains.in).
15
15
 
@@ -21,226 +21,45 @@ cd my-app
21
21
  npm run dev
22
22
  ```
23
23
 
24
- Your app runs at `http://localhost:3000` with a built-in visual dashboard at `http://localhost:3000/_dev`.
24
+ Your app runs at `http://localhost:3000`.
25
25
 
26
- ## CLI Commands
27
-
28
- ### Create a Project
26
+ ## Commands
29
27
 
30
28
  ```sh
31
- npx 4bnode init <name>
29
+ npx 4bnode add # interactive menu
32
30
  ```
33
31
 
34
- Scaffolds a new Express project with:
35
- - ES Module syntax
36
- - Environment config (`.env.development`, `.env.production`)
37
- - CORS, JSON body parser (50MB limit)
38
- - Static file serving from `public/`
39
- - Built-in dev dashboard at `/_dev`
40
-
41
- ### Add Features
42
-
43
- Use the interactive menu:
44
-
45
- ```sh
46
- npx 4bnode add
47
- ```
48
-
49
- Or add directly:
50
-
51
32
  | Command | Description |
52
33
  |---------|-------------|
53
- | `4bnode add api <name>` | Create a new API endpoint |
54
- | `4bnode add mongo` | Configure MongoDB connection |
34
+ | `4bnode init <name>` | Scaffold a new Express project |
35
+ | `4bnode add api <name>` | Create an API endpoint |
36
+ | `4bnode add mongo` | Configure MongoDB |
55
37
  | `4bnode add crud` | Add CRUD operations to a route |
56
- | `4bnode add login` | Add JWT authentication & login |
57
- | `4bnode add socket` | Integrate Socket.IO |
58
- | `4bnode add websocket` | Add native WebSocket support |
38
+ | `4bnode add login` | JWT authentication & login |
39
+ | `4bnode add docs` | OpenAPI docs at `/docs` |
40
+ | `4bnode add security` | Helmet, rate-limit, RBAC, API keys, validation |
41
+ | `4bnode add email` | Transactional email |
42
+ | `4bnode add socket` | Socket.IO |
43
+ | `4bnode add websocket` | Native WebSocket |
59
44
  | `4bnode add serialport` | Serial port communication |
45
+ | `4bnode schema [name]` | Create / edit Mongoose schemas |
46
+ | `4bnode list` | Show models, routes & config |
60
47
 
61
- ### API Endpoint Creation
62
-
63
- ```sh
64
- 4bnode add api users
65
- ```
66
-
67
- When creating an API endpoint:
68
- - Select HTTP method (GET, POST, PUT, DELETE, PATCH)
69
- - Choose fields from existing model (if available)
70
- - Add custom fields received from client
71
- - File fields auto-configure `multer` for multipart uploads
72
- - All routes use `/api/` prefix
73
-
74
- ### Schema Management
75
-
76
- ```sh
77
- 4bnode schema # Interactive create/view/edit
78
- 4bnode schema user # Create or edit 'user' schema
79
- ```
80
-
81
- Supported field types: `String`, `Number`, `Boolean`, `Date`, `Array`, `ObjectId`, `Mixed`, `Buffer`
82
-
83
- Field constraints: `required`, `unique`, `default`
84
-
85
- Presets available: `user`, `product`, `post`
86
-
87
- ### List Project Info
88
-
89
- ```sh
90
- 4bnode list # Show all models, routes & config
91
- 4bnode list models # List models only
92
- 4bnode list routes # List routes only
93
- ```
94
-
95
- ### Dashboard Passkey
96
-
97
- During `npx 4bnode init`, you'll be prompted to set a 6-digit passkey to protect the dev dashboard.
98
-
99
- To change the passkey later, run inside your project:
100
-
101
- ```sh
102
- 4bnode passkey
103
- ```
104
-
105
- ### Start Dev Dashboard
106
-
107
- ```sh
108
- 4bnode ui
109
- ```
110
-
111
- Runs `npm run dev` and opens the visual dashboard. Must be run inside a 4bnode project.
112
-
113
- ## Visual Dashboard
114
-
115
- Access at `http://localhost:3000/_dev` when running `npm run dev`.
116
-
117
- ### Dashboard Features
118
-
119
- **Overview** - Project status, configured features, quick stats
120
-
121
- **Database** - MongoDB connection setup, URI management
122
-
123
- **Schemas** - Create, view, edit MongoDB schemas with field management. Add/remove fields, toggle required/unique, set defaults.
124
-
125
- **API Routes** - Full route management:
126
- - Create new routes (Methods, CRUD, Login, Register)
127
- - Add methods to existing routes with field selection
128
- - Select fields from models or add custom fields
129
- - File upload fields auto-configure multer
130
- - View route source code, edit inline
131
- - Share endpoint docs with frontend team (Markdown format)
132
- - Delete routes (removes file + index.js registration)
133
-
134
- **API Tester** - Built-in Postman-like API testing tool:
135
- - REST API, Socket.IO, and WebSocket testing
136
- - **Body types**: JSON, Form Data, URL Encoded, Raw, Binary
137
- - **File uploads**: Choose file, send as File or Base64
138
- - **Auto-detect fields**: Matches URL to route and pre-fills body/form fields from route code
139
- - **Path variables**: Auto-detects `:id` params, shows editable fields
140
- - **Query params**: Key-value editor, auto-fills from `req.query`
141
- - **Headers**: Key-value editor with quick presets (Bearer Auth, JSON, Form Data, URL Encoded, Accept)
142
- - **Response viewer**: Body + Headers tabs, copy response, save responses
143
- - **Collection management**: Save requests, organize with responses, delete with confirmation
144
- - **Share collection**: Generate shareable API documentation page with:
145
- - Endpoint details, headers, body fields
146
- - Code examples (cURL, Fetch, Axios) with copy button
147
- - Form data / file upload code generation
148
- - Saved response examples
149
- - Search/filter endpoints
150
- - Persistent URL (updates on re-share)
151
- - **Import from routes**: One-click import endpoints from your project routes
152
- - All data persisted in `.4bnode/` JSON files (not localStorage)
153
-
154
- **Real-time** - Socket.IO and WebSocket configuration
155
-
156
- **SerialPort** - Serial port setup with path and baud rate config
157
-
158
- **Reports** - Export MongoDB data as Excel (.xlsx) reports:
159
- - Select models and fields to include
160
- - Include system fields (_id, createdAt, updatedAt)
161
- - Populate referenced ObjectId fields with sub-field selection
162
- - Custom column headers and field prefixes
163
- - Drag-and-drop column reordering
164
- - Configurable date format and timezone
165
- - Set row limit for large collections
166
- - Preview data before downloading
167
- - Download as `.xlsx` file
168
-
169
- **Logs** - Real-time server log monitoring:
170
- - Live log streaming via SSE (Server-Sent Events)
171
- - Filter by log type (LOG, INFO, WARN, ERROR)
172
- - Search through log entries
173
- - Auto-scroll with toggle
174
- - Keeps last 500 log entries
175
-
176
- **Environment** - View and edit `.env.development` and `.env.production`
177
-
178
- ## Generated Project Structure
48
+ ## Project Structure
179
49
 
180
50
  ```
181
51
  my-app/
182
- ├── index.js # Express app entry point
183
- ├── package.json
184
- ├── .env.development # Dev environment variables
185
- ├── .env.production # Production environment variables
186
- ├── .4bnode/ # Dashboard engine & data (managed by 4bnode)
187
- ├── public/ # Static files
188
- │ └── index.html # App status landing page
52
+ ├── index.js # Express entry point
53
+ ├── .env.development
54
+ ├── .env.production
55
+ ├── public/ # static files
189
56
  └── src/
190
- ├── db.js # MongoDB connection (after `add mongo`)
191
- ├── models/ # Mongoose schemas
192
- └── routes/ # API route files
193
- ```
194
-
195
- ## Real-time Communication
196
-
197
- ### Socket.IO
198
-
199
- ```sh
200
- 4bnode add socket
201
- ```
202
-
203
- Access in routes via `req.io`:
204
-
205
- ```js
206
- router.get('/notify', (req, res) => {
207
- req.io.emit('message', 'Hello from server');
208
- res.json({ sent: true });
209
- });
210
- ```
211
-
212
- ### WebSocket
213
-
214
- ```sh
215
- 4bnode add websocket
216
- ```
217
-
218
- Access in routes via `req.wss`:
219
-
220
- ```js
221
- router.get('/broadcast', (req, res) => {
222
- req.wss.clients.forEach(client => {
223
- if (client.readyState === WebSocket.OPEN) {
224
- client.send('Hello from server');
225
- }
226
- });
227
- res.json({ sent: true });
228
- });
229
- ```
230
-
231
- ## Help
232
-
233
- ```sh
234
- 4bnode -h
235
- 4bnode --help
57
+ ├── db.js # MongoDB connection
58
+ ├── models/ # Mongoose schemas
59
+ └── routes/ # API routes
236
60
  ```
237
61
 
238
62
  ## License
239
63
 
240
- Proprietary License
241
-
242
- (c) 2025 4Brains Technologies. All rights reserved.
243
-
244
- Unauthorized copying, distribution, modification, or use of this code is strictly prohibited.
245
-
246
- For licensing or commercial use: contact@4brains.in
64
+ Proprietary. © 2025 4Brains Technologies. All rights reserved.
65
+ Unauthorized copying, distribution, or use is prohibited. contact@4brains.in
package/add-crud.js CHANGED
@@ -18,112 +18,12 @@ import {
18
18
  showFileAction,
19
19
  installDeps,
20
20
  } from "./lib/ui.js";
21
-
22
- function buildInsertCode(pascalName, fields, dataSource) {
23
- const assignments = fields
24
- .map((f) => ` ${f}: ${dataSource}.${f}`)
25
- .join(",\n");
26
-
27
- let passwordLogic = "";
28
- if (fields.includes("password")) {
29
- passwordLogic = `
30
- if (newData.password) {
31
- const salt = await bcrypt.genSalt(10);
32
- newData.password = await bcrypt.hash(newData.password, salt);
33
- }`;
34
- }
35
-
36
- return `
37
- router.post('/create', async (req, res) => {
38
- const newData = {
39
- ${assignments}
40
- };
41
- ${passwordLogic}
42
- try {
43
- const newDocument = new ${pascalName}(newData);
44
- await newDocument.save();
45
- res.json({ message: 'Data inserted successfully', data: newDocument });
46
- } catch (err) {
47
- console.error(err);
48
- res.status(500).json({ message: 'Server error' });
49
- }
50
- });`;
51
- }
52
-
53
- function buildReadCode(pascalName) {
54
- return `
55
- router.get('/', async (req, res) => {
56
- try {
57
- const documents = await ${pascalName}.find();
58
- res.json({ data: documents });
59
- } catch (err) {
60
- console.error(err);
61
- res.status(500).json({ message: 'Server error' });
62
- }
63
- });
64
-
65
- router.get('/:id', async (req, res) => {
66
- try {
67
- const document = await ${pascalName}.findById(req.params.id);
68
- if (!document) {
69
- return res.status(404).json({ message: 'Document not found' });
70
- }
71
- res.json({ data: document });
72
- } catch (err) {
73
- console.error(err);
74
- res.status(500).json({ message: 'Server error' });
75
- }
76
- });`;
77
- }
78
-
79
- function buildUpdateCode(pascalName, fields, dataSource) {
80
- const assignments = fields
81
- .map((f) => ` ${f}: ${dataSource}.${f}`)
82
- .join(",\n");
83
-
84
- let passwordLogic = "";
85
- if (fields.includes("password")) {
86
- passwordLogic = `
87
- if (updatedData.password) {
88
- const salt = await bcrypt.genSalt(10);
89
- updatedData.password = await bcrypt.hash(updatedData.password, salt);
90
- }`;
91
- }
92
-
93
- return `
94
- router.put('/:id', async (req, res) => {
95
- const updatedData = {
96
- ${assignments}
97
- };
98
- ${passwordLogic}
99
- try {
100
- const updatedDocument = await ${pascalName}.findByIdAndUpdate(req.params.id, updatedData, { returnDocument: 'after' });
101
- if (!updatedDocument) {
102
- return res.status(404).json({ message: 'Document not found' });
103
- }
104
- res.json({ message: 'Data updated successfully', data: updatedDocument });
105
- } catch (err) {
106
- console.error(err);
107
- res.status(500).json({ message: 'Server error' });
108
- }
109
- });`;
110
- }
111
-
112
- function buildDeleteCode(pascalName) {
113
- return `
114
- router.delete('/:id', async (req, res) => {
115
- try {
116
- const deletedDocument = await ${pascalName}.findByIdAndDelete(req.params.id);
117
- if (!deletedDocument) {
118
- return res.status(404).json({ message: 'Document not found' });
119
- }
120
- res.json({ message: 'Data deleted successfully', data: deletedDocument });
121
- } catch (err) {
122
- console.error(err);
123
- res.status(500).json({ message: 'Server error' });
124
- }
125
- });`;
126
- }
21
+ import {
22
+ buildInsertCode,
23
+ buildReadCode,
24
+ buildUpdateCode,
25
+ buildDeleteCode,
26
+ } from "./lib/codegen.js";
127
27
 
128
28
  async function main() {
129
29
  showHeader("add-crud", "Add CRUD operations to a route");
package/add-docs.js ADDED
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { getProjectRoot, listRoutes, listModels, getModelSchemaDetailed } from "./lib/project.js";
6
+ import { addRouteRegistration, readIndex } from "./lib/indexFile.js";
7
+ import { showHeader, showTaskDone, showError, showFileAction, showInfo } from "./lib/ui.js";
8
+ import { extractRequestFields, buildOpenApiSpec, buildDocsRouter } from "./lib/codegen.js";
9
+
10
+ function collectEndpoints() {
11
+ const root = getProjectRoot();
12
+ const routesDir = path.join(root, "src", "routes");
13
+ if (!fs.existsSync(routesDir)) return [];
14
+ const indexContent = readIndex();
15
+ const out = [];
16
+ for (const file of listRoutes()) {
17
+ const name = path.basename(file, ".js");
18
+ const content = fs.readFileSync(path.join(routesDir, file), "utf8");
19
+ let prefix = "/api/" + name;
20
+ const reg = indexContent.match(new RegExp(`app\\.use\\(['"]([^'"]+)['"]\\s*,\\s*${name}Router`));
21
+ if (reg) prefix = reg[1];
22
+ const modelImport = content.match(/import\s+(\w+)\s+from\s+['"]\.\.\/models\/(\w+)(?:\.js)?['"]/);
23
+ const modelFields = modelImport ? getModelSchemaDetailed(modelImport[2]) : [];
24
+ const epRegex = /router\.(get|post|put|patch|delete)\(\s*['"`](\/[^'"`]*?)['"`]\s*,([\s\S]*?)(?=\nrouter\.|$)/gi;
25
+ let m;
26
+ while ((m = epRegex.exec(content)) !== null) {
27
+ const method = m[1].toUpperCase();
28
+ const epath = m[2];
29
+ const handler = m[3] || "";
30
+ const bodyFields = extractRequestFields(handler, "body").map((fn) => {
31
+ const meta = modelFields.find((x) => x.name === fn);
32
+ return { name: fn, type: meta?.type || "String", required: meta?.required || false };
33
+ });
34
+ const queryFields = extractRequestFields(handler, "query").map((fn) => ({ name: fn, type: "String" }));
35
+ const params = (epath.match(/:(\w+)/g) || []).map((p) => p.slice(1));
36
+ const fileFields = [];
37
+ const us = handler.match(/upload\.single\(\s*['"](\w+)['"]/);
38
+ if (us) fileFields.push({ name: us[1] });
39
+ const fullPath = (prefix + (epath === "/" ? "" : epath)).replace(/\/+/g, "/");
40
+ out.push({ method, path: epath, fullPath, bodyFields, queryFields, params, fileFields, hasAuth: /\bauth\b\s*,/.test(m[0]) });
41
+ }
42
+ }
43
+ return out;
44
+ }
45
+
46
+ function projectTitle() {
47
+ try {
48
+ const pkg = JSON.parse(fs.readFileSync(path.join(getProjectRoot(), "package.json"), "utf8"));
49
+ return (pkg.name || "API") + " API";
50
+ } catch { return "API"; }
51
+ }
52
+
53
+ async function main() {
54
+ showHeader("add-docs", "Generate OpenAPI docs + Swagger UI");
55
+
56
+ const root = getProjectRoot();
57
+ const srcDir = path.join(root, "src");
58
+ fs.mkdirSync(srcDir, { recursive: true });
59
+
60
+ const docsPath = path.join(srcDir, "docs.js");
61
+ if (!fs.existsSync(docsPath)) {
62
+ fs.writeFileSync(docsPath, buildDocsRouter(), "utf8");
63
+ showFileAction("created", "src/docs.js");
64
+ } else {
65
+ showInfo("src/docs.js already exists — keeping it.");
66
+ }
67
+
68
+ const endpoints = collectEndpoints();
69
+ const models = listModels().map((f) => {
70
+ const name = path.basename(f, ".js");
71
+ return { name, fields: getModelSchemaDetailed(name) };
72
+ });
73
+ const spec = buildOpenApiSpec({ title: projectTitle(), endpoints, models });
74
+ fs.writeFileSync(path.join(srcDir, "openapi.json"), JSON.stringify(spec, null, 2), "utf8");
75
+ showFileAction("created", "src/openapi.json");
76
+
77
+ addRouteRegistration("import docsRouter from './src/docs.js';", "app.use('/docs', docsRouter);");
78
+ showFileAction("updated", "index.js");
79
+
80
+ showTaskDone("API docs configured", [
81
+ `${endpoints.length} endpoint${endpoints.length === 1 ? "" : "s"} documented`,
82
+ "Swagger UI at /docs",
83
+ "Re-run after changing routes to refresh the spec",
84
+ ]);
85
+ }
86
+
87
+ main().catch((err) => {
88
+ if (err.name === "ExitPromptError") process.exit(0);
89
+ showError(err.message);
90
+ process.exit(1);
91
+ });
package/add-email.js ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import { input, select, confirm, password } from "@inquirer/prompts";
6
+ import { getProjectRoot } from "./lib/project.js";
7
+ import { updateEnvFile } from "./lib/env.js";
8
+ import { showHeader, showTaskDone, showError, showInfo, showFileAction, installDeps } from "./lib/ui.js";
9
+ import { MAIL_PROVIDERS, buildMailerService } from "./lib/codegen.js";
10
+
11
+ async function main() {
12
+ showHeader("add-email", "Set up transactional email (nodemailer)");
13
+
14
+ const providerId = await select({
15
+ message: "Email provider:",
16
+ choices: Object.entries(MAIL_PROVIDERS).map(([id, p]) => ({ name: p.label, value: id })),
17
+ });
18
+ const preset = MAIL_PROVIDERS[providerId];
19
+ if (preset.hint) showInfo(preset.hint);
20
+
21
+ const host = (await input({ message: "SMTP host:", default: preset.host || "" })).trim();
22
+ const port = (await input({ message: "SMTP port:", default: String(preset.port || 587) })).trim();
23
+ const secure = await confirm({ message: "Use TLS/secure (port 465)?", default: !!preset.secure });
24
+ const user = (await input({ message: "SMTP user:", default: preset.user || "" })).trim();
25
+ const pass = (await password({ message: "SMTP password / API key:", mask: "*" })).trim();
26
+ const from = (await input({ message: "From address:", default: user })).trim();
27
+
28
+ console.log();
29
+
30
+ const root = getProjectRoot();
31
+ const dir = path.join(root, "src", "services");
32
+ fs.mkdirSync(dir, { recursive: true });
33
+ const mailerPath = path.join(dir, "mailer.js");
34
+ if (!fs.existsSync(mailerPath)) {
35
+ fs.writeFileSync(mailerPath, buildMailerService(), "utf8");
36
+ showFileAction("created", "src/services/mailer.js");
37
+ } else {
38
+ showInfo("src/services/mailer.js already exists — keeping it.");
39
+ }
40
+
41
+ const envPath = path.join(root, ".env");
42
+ updateEnvFile(envPath, "SMTP_HOST", host);
43
+ updateEnvFile(envPath, "SMTP_PORT", port || "587");
44
+ updateEnvFile(envPath, "SMTP_SECURE", secure ? "true" : "false");
45
+ updateEnvFile(envPath, "SMTP_USER", user);
46
+ if (pass) updateEnvFile(envPath, "SMTP_PASS", pass);
47
+ updateEnvFile(envPath, "SMTP_FROM", from || user);
48
+ showFileAction("updated", ".env");
49
+
50
+ await installDeps("nodemailer");
51
+
52
+ showTaskDone("Email configured", [
53
+ `Provider: ${preset.label}`,
54
+ `Host: ${host}:${port || 587}`,
55
+ "Use sendMail({ to, subject, html }) from src/services/mailer.js",
56
+ ]);
57
+ }
58
+
59
+ main().catch((err) => {
60
+ if (err.name === "ExitPromptError") process.exit(0);
61
+ showError(err.message);
62
+ process.exit(1);
63
+ });
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import crypto from "crypto";
6
+ import { checkbox, confirm } from "@inquirer/prompts";
7
+ import { getProjectRoot, listModels, getModelSchemaDetailed } from "./lib/project.js";
8
+ import { updateEnvFile } from "./lib/env.js";
9
+ import { addImport, insertBeforeRoutes, insertBeforeListen } from "./lib/indexFile.js";
10
+ import {
11
+ showHeader,
12
+ showTaskDone,
13
+ showError,
14
+ showInfo,
15
+ showFileAction,
16
+ installDeps,
17
+ } from "./lib/ui.js";
18
+ import {
19
+ buildErrorHandlerFile,
20
+ buildRolesGuardFile,
21
+ buildApiKeyFile,
22
+ buildValidateFile,
23
+ buildZodSchemaFile,
24
+ securityWiring,
25
+ } from "./lib/codegen.js";
26
+
27
+ function writeFileSafe(relPath, content) {
28
+ const full = path.join(getProjectRoot(), relPath);
29
+ fs.mkdirSync(path.dirname(full), { recursive: true });
30
+ if (fs.existsSync(full)) {
31
+ showInfo(`${relPath} already exists — skipped.`);
32
+ return false;
33
+ }
34
+ fs.writeFileSync(full, content, "utf8");
35
+ showFileAction("created", relPath);
36
+ return true;
37
+ }
38
+
39
+ async function main() {
40
+ showHeader("add-security", "Harden your app with security middleware");
41
+
42
+ const features = await checkbox({
43
+ message: "Select protections to add:",
44
+ choices: [
45
+ { name: "Helmet Secure HTTP response headers", value: "helmet", checked: true },
46
+ { name: "Rate limiting Throttle requests per IP (express-rate-limit)", value: "rateLimit", checked: true },
47
+ { name: "Error handler Central 404 + error JSON responses", value: "errorHandler", checked: true },
48
+ { name: "RBAC roles roles('admin') route guard", value: "roles" },
49
+ { name: "API keys x-api-key header auth", value: "apiKey" },
50
+ { name: "Validation Zod request validation middleware", value: "validation" },
51
+ ],
52
+ validate: (v) => (v.length > 0 ? true : "Select at least one protection."),
53
+ });
54
+
55
+ console.log();
56
+
57
+ const projectRoot = getProjectRoot();
58
+ const deps = new Set();
59
+ const summary = [];
60
+
61
+ // ── App-level wiring (helmet / rate limit / error handler) ──
62
+ const appLevel = features.filter((f) => ["helmet", "rateLimit", "errorHandler"].includes(f));
63
+ if (appLevel.length) {
64
+ const wiring = securityWiring(appLevel);
65
+ wiring.imports.forEach((imp) => addImport(imp));
66
+ if (wiring.beforeRoutes) insertBeforeRoutes(wiring.beforeRoutes);
67
+ if (wiring.beforeListen) insertBeforeListen(wiring.beforeListen);
68
+ showFileAction("updated", "index.js");
69
+ }
70
+ if (features.includes("helmet")) { deps.add("helmet"); summary.push("Helmet security headers"); }
71
+ if (features.includes("rateLimit")) { deps.add("express-rate-limit"); summary.push("Rate limiting (100 req / 15 min per IP)"); }
72
+
73
+ // ── Middleware files ──
74
+ if (features.includes("errorHandler")) {
75
+ writeFileSafe("src/middleware/errorHandler.js", buildErrorHandlerFile());
76
+ summary.push("Central error handler");
77
+ }
78
+ if (features.includes("roles")) {
79
+ writeFileSafe("src/middleware/roles.js", buildRolesGuardFile());
80
+ summary.push("RBAC roles() guard");
81
+ }
82
+ if (features.includes("apiKey")) {
83
+ writeFileSafe("src/middleware/apiKey.js", buildApiKeyFile());
84
+ const envPath = path.join(projectRoot, ".env");
85
+ const existing = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf8") : "";
86
+ if (!/^API_KEYS=/m.test(existing)) {
87
+ const key = crypto.randomBytes(24).toString("hex");
88
+ updateEnvFile(envPath, "API_KEYS", key);
89
+ showFileAction("updated", ".env");
90
+ showInfo(`Generated API key: ${key}`);
91
+ }
92
+ summary.push("API key auth (x-api-key)");
93
+ }
94
+
95
+ // ── Validation (Zod) ──
96
+ if (features.includes("validation")) {
97
+ deps.add("zod");
98
+ writeFileSafe("src/middleware/validate.js", buildValidateFile());
99
+
100
+ const models = listModels().map((f) => path.basename(f, ".js"));
101
+ let generatedValidators = 0;
102
+ if (models.length) {
103
+ const wantValidators = await confirm({
104
+ message: "Generate Zod validators from existing models?",
105
+ default: true,
106
+ });
107
+ if (wantValidators) {
108
+ const chosen = await checkbox({
109
+ message: "Select models to generate validators for:",
110
+ choices: models.map((m) => ({ name: m, value: m, checked: true })),
111
+ });
112
+ for (const m of chosen) {
113
+ const fields = getModelSchemaDetailed(m);
114
+ if (!fields.length) {
115
+ showInfo(`${m}: no fields found — skipped.`);
116
+ continue;
117
+ }
118
+ if (writeFileSafe(`src/validators/${m}.js`, buildZodSchemaFile(m, fields))) {
119
+ generatedValidators++;
120
+ }
121
+ }
122
+ }
123
+ }
124
+ summary.push(
125
+ generatedValidators > 0
126
+ ? `Zod validation (+ ${generatedValidators} model validator${generatedValidators > 1 ? "s" : ""})`
127
+ : "Zod validation middleware"
128
+ );
129
+ }
130
+
131
+ if (deps.size) {
132
+ console.log();
133
+ await installDeps([...deps]);
134
+ }
135
+
136
+ showTaskDone("Security & middleware configured", summary);
137
+ }
138
+
139
+ main().catch((err) => {
140
+ if (err.name === "ExitPromptError") process.exit(0);
141
+ showError(err.message);
142
+ process.exit(1);
143
+ });