4bnode 4.1.2 → 4.1.4

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 (62) hide show
  1. package/add-security.js +0 -18
  2. package/app-1/.4bnode/.passkey +1 -0
  3. package/app-1/.4bnode/assets/fonts/NotoSans-Bold.woff2 +0 -0
  4. package/app-1/.4bnode/assets/fonts/NotoSans-Light.woff2 +0 -0
  5. package/app-1/.4bnode/assets/fonts/NotoSans-Medium.woff2 +0 -0
  6. package/app-1/.4bnode/assets/fonts/NotoSans-Regular.woff2 +0 -0
  7. package/app-1/.4bnode/assets/fonts/NotoSans-SemiBold.woff2 +0 -0
  8. package/app-1/.4bnode/assets/images/mongo.png +0 -0
  9. package/app-1/.4bnode/assets/js/App.js +139 -0
  10. package/app-1/.4bnode/assets/js/Root.js +40 -0
  11. package/app-1/.4bnode/assets/js/components/Modal.js +52 -0
  12. package/app-1/.4bnode/assets/js/components/Sidebar.js +73 -0
  13. package/app-1/.4bnode/assets/js/components/ToastContainer.js +15 -0
  14. package/app-1/.4bnode/assets/js/context.js +7 -0
  15. package/app-1/.4bnode/assets/js/lib.js +113 -0
  16. package/app-1/.4bnode/assets/js/main.js +5 -0
  17. package/app-1/.4bnode/assets/js/pages/AiBuilderPage.js +268 -0
  18. package/app-1/.4bnode/assets/js/pages/ApiTesterPage.js +1879 -0
  19. package/app-1/.4bnode/assets/js/pages/DashboardPage.js +435 -0
  20. package/app-1/.4bnode/assets/js/pages/DatabasePage.js +300 -0
  21. package/app-1/.4bnode/assets/js/pages/DocsPage.js +150 -0
  22. package/app-1/.4bnode/assets/js/pages/EnvironmentPage.js +103 -0
  23. package/app-1/.4bnode/assets/js/pages/LogsPage.js +205 -0
  24. package/app-1/.4bnode/assets/js/pages/MailPage.js +134 -0
  25. package/app-1/.4bnode/assets/js/pages/RealtimePage.js +186 -0
  26. package/app-1/.4bnode/assets/js/pages/RoutesPage.js +1436 -0
  27. package/app-1/.4bnode/assets/js/pages/SchemasPage.js +838 -0
  28. package/app-1/.4bnode/assets/js/pages/SecurityPage.js +74 -0
  29. package/app-1/.4bnode/assets/js/pages/SerialPortPage.js +170 -0
  30. package/app-1/.4bnode/assets/js/screens/PasskeyScreen.js +105 -0
  31. package/app-1/.4bnode/codegen.js +1249 -0
  32. package/app-1/.4bnode/dev-api.js +3621 -0
  33. package/app-1/.4bnode/ui.html +1578 -0
  34. package/app-1/README.md +47 -0
  35. package/app-1/ecosystem.config.cjs +25 -0
  36. package/app-1/index.js +145 -0
  37. package/app-1/package.json +35 -0
  38. package/app-1/public/index.html +292 -0
  39. package/app-1/src/db.js +13 -0
  40. package/app-1/src/docs.js +84 -0
  41. package/app-1/src/middleware/errorHandler.js +15 -0
  42. package/app-1/src/middleware/roles.js +14 -0
  43. package/app-1/src/middleware/validate.js +20 -0
  44. package/app-1/src/models/login.js +9 -0
  45. package/app-1/src/openapi.json +62 -0
  46. package/app-1/src/routes/products.js +26 -0
  47. package/app-1/src/validators/products.js +7 -0
  48. package/app-1/src/validators/user.js +7 -0
  49. package/index.js +2 -17
  50. package/lib/codegen.js +91 -44
  51. package/lib/codegen.test.js +23 -3
  52. package/package.json +1 -1
  53. package/skeleton/.4bnode/assets/js/App.js +0 -2
  54. package/skeleton/.4bnode/assets/js/components/Sidebar.js +2 -3
  55. package/skeleton/.4bnode/assets/js/lib.js +0 -3
  56. package/skeleton/.4bnode/assets/js/pages/DocsPage.js +29 -21
  57. package/skeleton/.4bnode/assets/js/pages/RoutesPage.js +12 -0
  58. package/skeleton/.4bnode/codegen.js +91 -44
  59. package/skeleton/.4bnode/dev-api.js +173 -87
  60. package/skeleton/_gitignore +0 -1
  61. package/skeleton/index.js +4 -8
  62. package/skeleton/.4bnode/assets/js/pages/ApiKeysPage.js +0 -164
@@ -0,0 +1,84 @@
1
+ import express from 'express';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import crypto from 'crypto';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ const router = express.Router();
8
+ const DIR = path.dirname(fileURLToPath(import.meta.url));
9
+ const SPEC = path.join(DIR, 'openapi.json');
10
+ const AUTH = path.join(DIR, 'docs-auth.json');
11
+
12
+ // Optional password protection. If src/docs-auth.json holds a password hash, the
13
+ // whole /docs surface requires a single password (NO username) via a small login
14
+ // page + cookie. Set/remove the password from the dashboard (API Docs). Read live
15
+ // (mtime-cached), so changes apply without a restart.
16
+ let _authCache = { mtimeMs: -1, hash: null };
17
+ function docsHash() {
18
+ try {
19
+ const { mtimeMs } = fs.statSync(AUTH);
20
+ if (mtimeMs !== _authCache.mtimeMs) {
21
+ const data = JSON.parse(fs.readFileSync(AUTH, 'utf8'));
22
+ _authCache = { mtimeMs, hash: (data && data.hash) || null };
23
+ }
24
+ } catch { _authCache = { mtimeMs: -1, hash: null }; }
25
+ return _authCache.hash;
26
+ }
27
+ function safeEq(a, b) {
28
+ try { const x = Buffer.from(String(a)), y = Buffer.from(String(b)); return x.length === y.length && crypto.timingSafeEqual(x, y); } catch { return false; }
29
+ }
30
+ function cookieToken(req) {
31
+ const m = (req.headers.cookie || '').match(/(?:^|; )docs_auth=([^;]+)/);
32
+ return m ? m[1] : null;
33
+ }
34
+ const LOGIN_HTML = "<!doctype html>\n<html lang=\"en\"><head>\n<meta charset=\"utf-8\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>API Documentation</title>\n<style>\n *{box-sizing:border-box}\n body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#0f1117;color:#eceef4;font-family:\"Noto Sans\",-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif}\n form{background:#1a1d2b;border:1px solid rgba(255,255,255,0.08);border-radius:14px;padding:28px;width:340px;max-width:90vw;text-align:center}\n h1{font-size:18px;margin:0 0 6px;background:linear-gradient(135deg,#3daefd,#0079ff);-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}\n p{color:#8b8fa3;font-size:13px;margin:0 0 18px}\n input{width:100%;background:#13161f;border:1px solid rgba(255,255,255,0.08);border-radius:9px;color:#eceef4;padding:11px 13px;font-size:14px;outline:none}\n input:focus{border-color:#0079ff}\n .err{color:#ef4444;font-size:12px;min-height:16px;margin:8px 0 0}\n button{width:100%;margin-top:14px;background:#0079ff;color:#fff;border:none;border-radius:9px;padding:11px;font-size:14px;font-weight:600;cursor:pointer}\n button:hover{filter:brightness(1.08)}\n</style></head>\n<body>\n<form method=\"post\" action=\"/docs/login\">\n <h1>API Documentation</h1>\n <p>Enter the password to view the docs.</p>\n <input type=\"password\" name=\"password\" placeholder=\"Password\" autofocus autocomplete=\"current-password\" />\n <div class=\"err\"><!--ERR--></div>\n <button type=\"submit\">Unlock</button>\n</form>\n</body></html>";
35
+
36
+ // Password submit — reachable without auth.
37
+ router.post('/login', (req, res) => {
38
+ const expected = docsHash();
39
+ if (!expected) return res.redirect('/docs');
40
+ const got = crypto.createHash('sha256').update(String((req.body && req.body.password) || '')).digest('hex');
41
+ if (safeEq(got, expected)) {
42
+ res.setHeader('Set-Cookie', 'docs_auth=' + expected + '; Path=/docs; HttpOnly; SameSite=Lax; Max-Age=86400');
43
+ return res.redirect('/docs');
44
+ }
45
+ return res.status(401).type('html').send(LOGIN_HTML.replace('<!--ERR-->', 'Incorrect password'));
46
+ });
47
+
48
+ // Gate the rest of /docs behind the password (when one is set).
49
+ router.use((req, res, next) => {
50
+ const expected = docsHash();
51
+ if (!expected) return next(); // no password → open
52
+ if (safeEq(cookieToken(req), expected)) return next();
53
+ return res.status(401).type('html').send(LOGIN_HTML);
54
+ });
55
+
56
+ // Interactive API documentation. The page is fully self-contained (no external
57
+ // CDN); the spec is generated by 4bnode and served from this app.
58
+ router.get('/openapi.json', (req, res) => {
59
+ res.set('Cache-Control', 'no-store');
60
+ if (!fs.existsSync(SPEC)) return res.status(404).json({ error: 'API spec not generated yet.' });
61
+ res.sendFile(SPEC);
62
+ });
63
+
64
+ const DOCS_PAGE = "<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>API Documentation</title>\n<style>\n * { box-sizing: border-box; }\n :root {\n --bg:#0f1117; --surface:#11141e; --card:#1a1d2b; --card-hover:#21253a; --input:#13161f;\n --border:rgba(255,255,255,0.08); --text:#eceef4; --muted:#8b8fa3; --faint:#626880;\n --accent:#0079ff; --accent-light:#3daefd; --accent-glow:rgba(0,121,255,0.14);\n }\n * { scrollbar-width: thin; }\n html, body { margin:0; padding:0; height:100%; }\n body { background:var(--bg); color:var(--text); line-height:1.5; height:100vh;\n display:flex; flex-direction:column; overflow:hidden;\n font-family:\"Noto Sans\",-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,sans-serif; }\n .topbar { flex:none; display:flex; align-items:center; gap:14px;\n background:var(--surface); border-bottom:1px solid var(--border); padding:14px 24px; }\n .topbar h1 { font-size:17px; font-weight:700; margin:0;\n background:linear-gradient(135deg,#3daefd,#0079ff); -webkit-background-clip:text;\n background-clip:text; -webkit-text-fill-color:transparent; }\n .pill-ver { font-size:12px; color:var(--accent-light); background:rgba(0,121,255,0.12);\n border:1px solid rgba(0,121,255,0.25); padding:3px 9px; border-radius:999px; }\n .layout { flex:1; display:flex; min-height:0; }\n .sidebar { width:330px; flex:none; border-right:1px solid var(--border);\n overflow-y:auto; padding:14px 12px; }\n .search { width:100%; background:var(--input); border:1px solid var(--border);\n color:var(--text); border-radius:9px; padding:8px 12px; font-size:13px; outline:none; margin-bottom:12px; }\n .search:focus { border-color:var(--accent); }\n .group-title { font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:0.6px;\n color:var(--faint); margin:14px 6px 6px; }\n .nav-item { display:flex; align-items:center; gap:9px; padding:8px 10px; border-radius:8px;\n cursor:pointer; border:1px solid transparent; }\n .nav-item:hover { background:var(--card-hover); }\n .nav-item.active { background:var(--accent-glow); border-color:rgba(0,121,255,0.35); }\n .nav-method { font-size:9.5px; font-weight:700; letter-spacing:0.4px; text-transform:uppercase;\n color:#fff; padding:3px 0; border-radius:5px; width:46px; flex:none; text-align:center; }\n .nav-path { font-family:ui-monospace,Menlo,monospace; font-size:12.5px; color:var(--text);\n overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }\n .main { flex:1; overflow-y:auto; padding:30px 40px; }\n .main-empty { color:var(--faint); display:flex; height:100%; align-items:center; justify-content:center; }\n .detail-head { display:flex; align-items:center; gap:14px; margin-bottom:6px; }\n .method-lg { font-size:13px; font-weight:700; letter-spacing:0.5px; text-transform:uppercase;\n color:#fff; padding:6px 12px; border-radius:8px; }\n .path-lg { font-family:ui-monospace,Menlo,monospace; font-size:18px; color:var(--text); word-break:break-all; }\n .detail-sum { color:var(--muted); margin:0 0 8px; }\n .lock { color:var(--faint); font-size:12px; margin-left:8px; white-space:nowrap; }\n .sec { font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:0.5px;\n color:var(--faint); margin:22px 0 8px; }\n .row { display:flex; gap:10px; align-items:center; padding:8px 0;\n border-bottom:1px solid var(--border); font-size:13px; }\n .row:last-child { border-bottom:none; }\n .fname { font-family:ui-monospace,Menlo,monospace; color:var(--text); min-width:150px; }\n .ftype { color:var(--accent-light); font-size:12px; }\n .req { color:#ef4444; font-size:11px; font-weight:700; }\n .opt { color:var(--faint); font-size:11px; }\n .resp-code { font-family:ui-monospace,Menlo,monospace; color:#22c55e; font-weight:700; }\n input.f, textarea.f { width:100%; max-width:540px; background:var(--input); border:1px solid var(--border);\n color:var(--text); border-radius:8px; padding:8px 10px; font-size:13px; outline:none;\n font-family:ui-monospace,Menlo,monospace; }\n input.f:focus, textarea.f:focus { border-color:var(--accent); }\n textarea.f { min-height:110px; resize:vertical; }\n .try-field { margin-bottom:10px; }\n .try-label { font-size:12px; color:var(--muted); margin-bottom:4px; display:block; }\n .btn { background:var(--accent); color:#fff; border:none; border-radius:8px; padding:9px 18px;\n font-size:13px; font-weight:600; cursor:pointer; }\n .btn:hover { filter:brightness(1.08); }\n .btn:disabled { opacity:.5; cursor:not-allowed; }\n pre.out { background:#0a0c12; border:1px solid var(--border); border-radius:8px; padding:12px;\n font-size:12.5px; color:#d5d8e3; overflow:auto; max-height:360px; max-width:540px; white-space:pre-wrap;\n word-break:break-word; margin-top:10px; }\n .out-status { font-family:ui-monospace,Menlo,monospace; font-size:12px; margin-top:10px; }\n ::-webkit-scrollbar { width:9px; height:9px; }\n ::-webkit-scrollbar-thumb { background:rgba(255,255,255,0.12); border-radius:6px; }\n</style>\n</head>\n<body>\n<div class=\"topbar\">\n <h1 id=\"apiTitle\">API</h1>\n <span class=\"pill-ver\" id=\"apiVersion\">v1.0.0</span>\n</div>\n<div class=\"layout\">\n <aside class=\"sidebar\">\n <input class=\"search\" id=\"search\" placeholder=\"Filter endpoints...\" />\n <div id=\"nav\"></div>\n </aside>\n <main class=\"main\" id=\"main\"><div class=\"main-empty\">Select an endpoint to view its details.</div></main>\n</div>\n<script>\n(function(){\n var SPEC_URL='/docs/openapi.json';\n var COLORS={get:'#0079ff',post:'#22c55e',put:'#f59e0b',patch:'#a855f7',delete:'#ef4444',head:'#64748b',options:'#64748b'};\n var uid=0; var ITEMS=[];\n function esc(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;');}\n function typeName(sc){if(!sc)return'';if(sc.format==='binary')return'file';if(sc.$ref)return sc.$ref.split('/').pop();return sc.type||'string';}\n function paramsOf(op,where){return (op.parameters||[]).filter(function(p){return p.in===where;});}\n function bodyOf(op){if(!op.requestBody)return null;var c=op.requestBody.content||{};var mime=Object.keys(c)[0];if(!mime)return null;return {mime:mime,schema:(c[mime]||{}).schema||{}};}\n function sampleVal(sc){var t=typeName(sc);if(t==='integer'||t==='number')return 0;if(t==='boolean')return false;return'';}\n function bodySkeleton(schema){var o={};var p=(schema&&schema.properties)||{};Object.keys(p).forEach(function(k){o[k]=sampleVal(p[k]);});return JSON.stringify(o,null,2);}\n function groupKey(p){return (p.split('/').filter(Boolean)[0]||'general').replace(/[{}]/g,'');}\n function collect(spec){var out=[];var paths=spec.paths||{};Object.keys(paths).forEach(function(p){var item=paths[p];Object.keys(item).forEach(function(m){out.push({path:p,method:m,op:item[m]});});});return out;}\n\n function detailsHtml(e,id){\n var html='';\n var pp=paramsOf(e.op,'path'),qp=paramsOf(e.op,'query'),body=bodyOf(e.op);\n if(pp.length||qp.length){\n html+='<div class=\"sec\">Parameters</div>';\n pp.concat(qp).forEach(function(p){\n html+='<div class=\"row\"><span class=\"fname\">'+esc(p.name)+'</span><span class=\"ftype\">'+esc(typeName(p.schema)||'string')+' &middot; '+esc(p.in)+'</span>'+(p.required?'<span class=\"req\">required</span>':'<span class=\"opt\">optional</span>')+'</div>';\n });\n }\n if(body){\n html+='<div class=\"sec\">Request body <span class=\"opt\">('+esc(body.mime)+')</span></div>';\n var props=(body.schema&&body.schema.properties)||{};var reqd=(body.schema&&body.schema.required)||[];\n Object.keys(props).forEach(function(k){\n html+='<div class=\"row\"><span class=\"fname\">'+esc(k)+'</span><span class=\"ftype\">'+esc(typeName(props[k]))+'</span>'+(reqd.indexOf(k)>=0?'<span class=\"req\">required</span>':'<span class=\"opt\">optional</span>')+'</div>';\n });\n }\n html+='<div class=\"sec\">Responses</div>';\n var resp=e.op.responses||{};\n Object.keys(resp).forEach(function(code){\n html+='<div class=\"row\"><span class=\"resp-code\">'+esc(code)+'</span><span class=\"detail-sum\" style=\"margin:0\">'+esc((resp[code]||{}).description||'')+'</span></div>';\n });\n html+='<div class=\"sec\">Try it</div>';\n pp.forEach(function(p){html+='<div class=\"try-field\"><label class=\"try-label\">'+esc(p.name)+' (path)</label><input class=\"f\" data-kind=\"path\" data-name=\"'+esc(p.name)+'\" /></div>';});\n qp.forEach(function(p){html+='<div class=\"try-field\"><label class=\"try-label\">'+esc(p.name)+' (query)</label><input class=\"f\" data-kind=\"query\" data-name=\"'+esc(p.name)+'\" /></div>';});\n if(e.op.security){html+='<div class=\"try-field\"><label class=\"try-label\">Authorization (Bearer token)</label><input class=\"f\" id=\"'+id+'_auth\" placeholder=\"token\" /></div>';}\n if(body){html+='<div class=\"try-field\"><label class=\"try-label\">Body (JSON)</label><textarea class=\"f\" id=\"'+id+'_body\">'+esc(bodySkeleton(body.schema))+'</textarea></div>';}\n html+='<button class=\"btn\" id=\"'+id+'_send\">Send request</button>';\n html+='<div class=\"out-status\" id=\"'+id+'_status\"></div>';\n html+='<pre class=\"out\" id=\"'+id+'_out\" style=\"display:none\"></pre>';\n return html;\n }\n\n function wireTry(e,id,root){\n var btn=root.querySelector('#'+id+'_send');if(!btn)return;\n // Auto-grow the body textarea to fit its content (capped, then scrolls).\n var bodyTa=root.querySelector('#'+id+'_body');\n if(bodyTa){\n var autosize=function(){ bodyTa.style.height='auto'; bodyTa.style.height=Math.min(bodyTa.scrollHeight+2,600)+'px'; };\n bodyTa.addEventListener('input',autosize);\n autosize();\n }\n btn.addEventListener('click',function(){\n var url=e.path;\n root.querySelectorAll('[data-kind=path]').forEach(function(inp){url=url.replace('{'+inp.getAttribute('data-name')+'}',encodeURIComponent(inp.value||''));});\n var qs=[];\n root.querySelectorAll('[data-kind=query]').forEach(function(inp){if(inp.value)qs.push(encodeURIComponent(inp.getAttribute('data-name'))+'='+encodeURIComponent(inp.value));});\n if(qs.length)url+=(url.indexOf('?')>=0?'&':'?')+qs.join('&');\n var opts={method:e.method.toUpperCase(),headers:{}};\n var auth=root.querySelector('#'+id+'_auth');if(auth&&auth.value)opts.headers['Authorization']='Bearer '+auth.value;\n var bodyEl=root.querySelector('#'+id+'_body');\n if(bodyEl&&['post','put','patch'].indexOf(e.method)>=0){opts.headers['Content-Type']='application/json';opts.body=bodyEl.value;}\n var statusEl=root.querySelector('#'+id+'_status');var outEl=root.querySelector('#'+id+'_out');\n btn.disabled=true;statusEl.textContent='Sending...';var t0=Date.now();\n fetch(url,opts).then(function(r){var ct=r.headers.get('content-type')||'';return r.text().then(function(txt){return {r:r,txt:txt,ct:ct};});}).then(function(d){\n var color=d.r.ok?'#22c55e':'#ef4444';\n statusEl.innerHTML='<span style=\"color:'+color+'\">'+d.r.status+' '+esc(d.r.statusText)+'</span> &middot; '+(Date.now()-t0)+'ms';\n var out=d.txt;if(d.ct.indexOf('application/json')>=0){try{out=JSON.stringify(JSON.parse(d.txt),null,2);}catch(err){}}\n outEl.style.display='block';outEl.textContent=out;\n }).catch(function(err){statusEl.innerHTML='<span style=\"color:#ef4444\">Request failed</span>';outEl.style.display='block';outEl.textContent=String(err);}).then(function(){btn.disabled=false;});\n });\n }\n\n function showDetail(e,navEl){\n document.querySelectorAll('.nav-item').forEach(function(n){n.classList.remove('active');});\n if(navEl)navEl.classList.add('active');\n var id='ep'+(uid++);\n var main=document.getElementById('main');\n var color=COLORS[e.method]||'#64748b';\n var lock=e.op.security?'<span class=\"lock\">&#128274; requires auth</span>':'';\n var html='<div class=\"detail-head\"><span class=\"method-lg\" style=\"background:'+color+'\">'+esc(e.method)+'</span><span class=\"path-lg\">'+esc(e.path)+'</span></div>';\n html+='<div class=\"detail-sum\">'+esc(e.op.summary||'')+lock+'</div>';\n html+=detailsHtml(e,id);\n main.innerHTML=html;main.scrollTop=0;\n wireTry(e,id,main);\n }\n\n function render(spec){\n document.getElementById('apiTitle').textContent=(spec.info&&spec.info.title)||'API';\n document.getElementById('apiVersion').textContent='v'+((spec.info&&spec.info.version)||'1.0.0');\n ITEMS=collect(spec);\n var nav=document.getElementById('nav');nav.innerHTML='';\n if(!ITEMS.length){\n nav.innerHTML='<div class=\"group-title\">No endpoints</div>';\n document.getElementById('main').innerHTML='<div class=\"main-empty\">No endpoints in the spec yet. Add routes, then regenerate the docs from the dashboard (API Docs).</div>';\n return;\n }\n var groups={};ITEMS.forEach(function(e){var k=groupKey(e.path);(groups[k]=groups[k]||[]).push(e);});\n var first=null;\n Object.keys(groups).sort().forEach(function(k){\n var gt=document.createElement('div');gt.className='group-title';gt.textContent=k;nav.appendChild(gt);\n groups[k].forEach(function(e){\n var color=COLORS[e.method]||'#64748b';\n var item=document.createElement('div');item.className='nav-item';\n item.setAttribute('data-search',(e.method+' '+e.path+' '+(e.op.summary||'')).toLowerCase());\n item.innerHTML='<span class=\"nav-method\" style=\"background:'+color+'\">'+esc(e.method)+'</span><span class=\"nav-path\">'+esc(e.path)+'</span>';\n item.addEventListener('click',function(){showDetail(e,item);});\n nav.appendChild(item);\n if(!first)first={e:e,item:item};\n });\n });\n if(first)showDetail(first.e,first.item);\n }\n\n document.getElementById('search').addEventListener('input',function(ev){\n var q=ev.target.value.toLowerCase();\n document.querySelectorAll('.nav-item').forEach(function(c){c.style.display=c.getAttribute('data-search').indexOf(q)>=0?'':'none';});\n });\n\n // The server inlines the spec below (so a protected page needs no second\n // authenticated request — avoids cross-browser cookie quirks on fetch). If it\n // isn't inlined (e.g. the page is served statically), fall back to fetching.\n var SPEC_DATA=/*__SPEC__*/null;\n if(SPEC_DATA&&typeof SPEC_DATA==='object'){\n render(SPEC_DATA);\n } else {\n fetch(SPEC_URL,{credentials:'same-origin'}).then(function(r){\n if(!r.ok){throw new Error('the server returned HTTP '+r.status+'. Regenerate the docs from the dashboard (API Docs).');}\n return r.json();\n }).then(render).catch(function(err){\n document.getElementById('main').innerHTML='<div class=\"main-empty\">Could not load the API spec &mdash; '+esc(String((err&&err.message)||err))+'</div>';\n });\n }\n})();\n</script>\n</body>\n</html>";
65
+
66
+ router.get('/', (req, res) => {
67
+ // Inline the spec into the page so a password-protected page doesn't need a
68
+ // second authenticated request (some browsers won't attach the docs cookie to
69
+ // the fetch). Falls back to fetching /openapi.json if no spec is inlined.
70
+ let spec = 'null';
71
+ try {
72
+ if (fs.existsSync(SPEC)) {
73
+ const raw = fs.readFileSync(SPEC, 'utf8').trim();
74
+ // Escape '<' so a spec value containing '</script>' can't break out of the
75
+ // inline <script>. The '\u003c' sequence still evaluates to '<' in JS.
76
+ if (raw) spec = raw.split('<').join(String.fromCharCode(92) + 'u003c');
77
+ }
78
+ } catch {}
79
+ // Never cache the docs page — keeps it from going stale after a regenerate.
80
+ res.set('Cache-Control', 'no-store');
81
+ res.type('html').send(DOCS_PAGE.replace('/*__SPEC__*/null', () => spec));
82
+ });
83
+
84
+ export default router;
@@ -0,0 +1,15 @@
1
+ // Central error handling. Register AFTER all routes:
2
+ // app.use(notFound);
3
+ // app.use(errorHandler);
4
+ export function notFound(req, res, next) {
5
+ res.status(404).json({ message: `Not found - ${req.originalUrl}` });
6
+ }
7
+
8
+ export function errorHandler(err, req, res, next) {
9
+ const status = err.status || err.statusCode || 500;
10
+ console.error(err);
11
+ res.status(status).json({
12
+ message: err.message || 'Server error',
13
+ ...(process.env.NODE_ENV === 'production' ? {} : { stack: err.stack }),
14
+ });
15
+ }
@@ -0,0 +1,14 @@
1
+ // Role-based access control. Use AFTER the JWT auth middleware so req.user is set:
2
+ // router.get('/admin', auth, roles('admin'), handler);
3
+ // Requires the login token payload to include a `role` (e.g. jwt.sign({ id, role })).
4
+ const roles = (...allowed) => (req, res, next) => {
5
+ if (!req.user) {
6
+ return res.status(401).json({ message: 'Not authenticated' });
7
+ }
8
+ if (allowed.length && !allowed.includes(req.user.role)) {
9
+ return res.status(403).json({ message: 'Forbidden: insufficient role' });
10
+ }
11
+ next();
12
+ };
13
+
14
+ export default roles;
@@ -0,0 +1,20 @@
1
+ import { ZodError } from 'zod';
2
+
3
+ // Validate part of the request against a Zod schema. Replaces req[source] with the
4
+ // parsed/coerced data on success. Use as middleware:
5
+ // router.post('/', validate(userSchema), handler); // validates req.body
6
+ // router.get('/', validate(querySchema, 'query'), handler); // validates req.query
7
+ const validate = (schema, source = 'body') => (req, res, next) => {
8
+ const result = schema.safeParse(req[source]);
9
+ if (!result.success) {
10
+ const errors = (result.error instanceof ZodError ? result.error.issues : []).map((i) => ({
11
+ path: i.path.join('.'),
12
+ message: i.message,
13
+ }));
14
+ return res.status(400).json({ message: 'Validation failed', errors });
15
+ }
16
+ req[source] = result.data;
17
+ next();
18
+ };
19
+
20
+ export default validate;
@@ -0,0 +1,9 @@
1
+ import mongoose from 'mongoose';
2
+
3
+ const LoginSchema = new mongoose.Schema({
4
+ first_name: { type: String }
5
+ }, { timestamps: true });
6
+
7
+ const Login = mongoose.model('Login', LoginSchema);
8
+
9
+ export default Login;
@@ -0,0 +1,62 @@
1
+ {
2
+ "openapi": "3.0.0",
3
+ "info": {
4
+ "title": "app-1",
5
+ "version": "1.0.0"
6
+ },
7
+ "servers": [
8
+ {
9
+ "url": "/"
10
+ }
11
+ ],
12
+ "paths": {
13
+ "/api/products": {
14
+ "post": {
15
+ "summary": "POST /api/products",
16
+ "responses": {
17
+ "200": {
18
+ "description": "Success"
19
+ }
20
+ },
21
+ "requestBody": {
22
+ "content": {
23
+ "application/json": {
24
+ "schema": {
25
+ "type": "object",
26
+ "properties": {
27
+ "first_name": {
28
+ "type": "string"
29
+ }
30
+ }
31
+ }
32
+ }
33
+ }
34
+ },
35
+ "security": [
36
+ {
37
+ "bearerAuth": []
38
+ }
39
+ ]
40
+ }
41
+ }
42
+ },
43
+ "components": {
44
+ "securitySchemes": {
45
+ "bearerAuth": {
46
+ "type": "http",
47
+ "scheme": "bearer",
48
+ "bearerFormat": "JWT"
49
+ }
50
+ },
51
+ "schemas": {
52
+ "Login": {
53
+ "type": "object",
54
+ "properties": {
55
+ "first_name": {
56
+ "type": "string"
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,26 @@
1
+ import { productsSchema } from '../validators/products.js';
2
+ import validate from '../middleware/validate.js';
3
+ import express from 'express';
4
+ import jwt from 'jsonwebtoken';
5
+ import Login from '../models/login.js';
6
+
7
+ const router = express.Router();
8
+
9
+ // Register
10
+ router.post('/', validate(productsSchema), async (req, res) => {
11
+ try {
12
+ const { first_name } = req.body;
13
+ const existing = await Login.findOne({ first_name });
14
+ if (existing) return res.status(400).json({ message: 'Login already exists' });
15
+
16
+ const item = new Login({ first_name });
17
+ const saved = await item.save();
18
+
19
+ const token = jwt.sign({ id: saved._id }, process.env.JWT_SECRET, { expiresIn: '7d' });
20
+ res.status(201).json({ token, login: { id: saved._id, first_name: saved.first_name } });
21
+ } catch (err) {
22
+ res.status(400).json({ message: err.message });
23
+ }
24
+ });
25
+
26
+ export default router;
@@ -0,0 +1,7 @@
1
+ import { z } from 'zod';
2
+
3
+ export const productsSchema = z.object({
4
+ first_name: z.string(),
5
+ });
6
+
7
+ export default productsSchema;
@@ -0,0 +1,7 @@
1
+ import { z } from 'zod';
2
+
3
+ export const userSchema = z.object({
4
+ first_name: z.string(),
5
+ });
6
+
7
+ export default userSchema;
package/index.js CHANGED
@@ -116,20 +116,6 @@ function savePasskey(projectPath, passkey) {
116
116
  fs.writeFileSync(path.join(projectPath, ".4bnode", ".passkey"), hash, "utf8");
117
117
  }
118
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
-
133
119
  async function createSkeletonApp(projectName) {
134
120
  showBanner();
135
121
 
@@ -156,7 +142,6 @@ async function createSkeletonApp(projectName) {
156
142
  console.log();
157
143
  const passkey = await promptPasskey();
158
144
  savePasskey(targetPath, passkey);
159
- seedApiKey(targetPath);
160
145
  console.log(chalk.hex("#00f260").bold(" ✔ Dashboard passkey set"));
161
146
 
162
147
  const installSpinner = createSpinner("Installing dependencies...");
@@ -216,7 +201,7 @@ const addOptions = [
216
201
  requiresRoutes: true,
217
202
  },
218
203
  {
219
- name: "Security Helmet, rate-limit, RBAC, API keys, validation",
204
+ name: "Security Helmet, rate-limit, RBAC, validation",
220
205
  value: "security",
221
206
  script: "add-security.js",
222
207
  },
@@ -326,7 +311,7 @@ function showHelp() {
326
311
  console.log(` ${c("add crud")} CRUD routes (create/read/update/delete)`);
327
312
  console.log(` ${c("add login")} JWT authentication & login`);
328
313
  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`);
314
+ console.log(` ${c("add security")} Helmet, rate-limit, RBAC, validation`);
330
315
  console.log(` ${c("add socket")} Socket.IO integration`);
331
316
  console.log(` ${c("add websocket")} Native WebSocket support`);
332
317
  console.log(` ${c("add serialport")} Serial port communication`);
package/lib/codegen.js CHANGED
@@ -274,44 +274,26 @@ export default roles;
274
274
  `;
275
275
  }
276
276
 
277
- // src/middleware/apiKey.js — static API key auth via x-api-key header.
278
- export function buildApiKeyFile() {
279
- return `// API key authentication. Named keys are managed by the 4bnode dashboard and
280
- // stored in src/api-keys.json. Send a key in the \`x-api-key\` request header:
281
- // import apiKey from './src/middleware/apiKey.js';
282
- // app.use('/api/private', apiKey);
283
- import fs from 'fs';
284
- import path from 'path';
285
- import { fileURLToPath } from 'url';
286
-
287
- const KEYS_FILE = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'api-keys.json');
288
- let _cache = { mtimeMs: -1, keys: [] };
289
-
290
- // Read the keys file fresh when it changes (mtime-cached), so keys generated or
291
- // revoked in the dashboard take effect immediately — no restart needed.
292
- function loadKeys() {
293
- try {
294
- const { mtimeMs } = fs.statSync(KEYS_FILE);
295
- if (mtimeMs !== _cache.mtimeMs) {
296
- const list = JSON.parse(fs.readFileSync(KEYS_FILE, 'utf8'));
297
- _cache = { mtimeMs, keys: (Array.isArray(list) ? list : []).map((k) => k && k.key).filter(Boolean) };
298
- }
299
- } catch {
300
- _cache = { mtimeMs: -1, keys: [] };
277
+ // src/middleware/auth.js — JWT verification. Reads a Bearer token, verifies it
278
+ // with JWT_SECRET, and sets req.user. Use as middleware: router.get('/', auth, h).
279
+ export function buildAuthMiddleware() {
280
+ return `import jwt from 'jsonwebtoken';
281
+
282
+ const auth = (req, res, next) => {
283
+ const token = req.header('Authorization')?.replace('Bearer ', '');
284
+ if (!token) {
285
+ return res.status(401).json({ message: 'No token, authorization denied' });
301
286
  }
302
- return _cache.keys;
303
- }
304
-
305
- const apiKey = (req, res, next) => {
306
- const provided = req.header('x-api-key');
307
- const valid = loadKeys();
308
- if (!provided || !valid.includes(provided)) {
309
- return res.status(401).json({ message: 'Invalid or missing API key' });
287
+ try {
288
+ const decoded = jwt.verify(token, process.env.JWT_SECRET);
289
+ req.user = decoded;
290
+ next();
291
+ } catch (err) {
292
+ res.status(401).json({ message: 'Token is not valid' });
310
293
  }
311
- next();
312
294
  };
313
295
 
314
- export default apiKey;
296
+ export default auth;
315
297
  `;
316
298
  }
317
299
 
@@ -406,6 +388,22 @@ export function wireValidateIntoRouteContent(content, modelBase) {
406
388
  return out;
407
389
  }
408
390
 
391
+ // Protect a whole route file with JWT: insert `auth` as the FIRST middleware on
392
+ // every endpoint (so it runs before validation), and add the import. Idempotent.
393
+ export function wireAuthIntoRouteContent(content) {
394
+ // (router.<method>('/path', )(existing middleware)(handler arrow)
395
+ const handlerRe =
396
+ /(router\.(?:get|post|put|patch|delete)\(\s*['"`][^'"`]*['"`]\s*,\s*)([\s\S]*?)((?:async\s*)?\(\s*req\s*,\s*res\s*(?:,\s*next\s*)?\)\s*=>)/g;
397
+ let changed = false;
398
+ const replaced = content.replace(handlerRe, (full, pathPart, middleware, handler) => {
399
+ if (/\bauth\s*,/.test(middleware)) return full; // already protected
400
+ changed = true;
401
+ return `${pathPart}auth, ${middleware}${handler}`;
402
+ });
403
+ if (!changed) return content;
404
+ return addImportToContent(replaced, "import auth from '../middleware/auth.js';");
405
+ }
406
+
409
407
  // Anchor comment that marks the central error handler in index.js. Route
410
408
  // registration looks for this so new routes are always inserted ABOVE it.
411
409
  export const ERROR_HANDLER_MARKER = "// ── Error handling (must be last) ──";
@@ -421,7 +419,28 @@ export function securityWiring(features) {
421
419
 
422
420
  if (has("helmet")) {
423
421
  imports.push("import helmet from 'helmet';");
424
- beforeRoutesParts.push("// ── Security headers ──\napp.use(helmet());");
422
+ beforeRoutesParts.push(
423
+ `// ── Security headers ──
424
+ // helmet() defaults to a strict Content-Security-Policy (script-src 'self',
425
+ // etc.) that blocks CDN-loaded scripts, inline <script> blocks, and cross-origin
426
+ // assets. We widen the policy so a typical frontend/docs page works while keeping
427
+ // the rest of helmet's protections. Tighten these directives for production.
428
+ app.use(helmet({
429
+ contentSecurityPolicy: {
430
+ directives: {
431
+ defaultSrc: ["'self'"],
432
+ scriptSrc: ["'self'", 'https:', "'unsafe-inline'"],
433
+ styleSrc: ["'self'", 'https:', "'unsafe-inline'"],
434
+ imgSrc: ["'self'", 'data:', 'https:'],
435
+ connectSrc: ["'self'"],
436
+ },
437
+ },
438
+ crossOriginEmbedderPolicy: false,
439
+ // Allow other origins (e.g. a separate frontend host) to load assets served
440
+ // by this app. Default is 'same-origin'. Tighten to 'same-site' for production.
441
+ crossOriginResourcePolicy: { policy: 'cross-origin' },
442
+ }));`
443
+ );
425
444
  }
426
445
  if (has("rateLimit")) {
427
446
  imports.push("import rateLimit from 'express-rate-limit';");
@@ -960,7 +979,6 @@ export function buildDocsHtml(specUrl = "openapi.json") {
960
979
  html+='<div class="row"><span class="resp-code">'+esc(code)+'</span><span class="detail-sum" style="margin:0">'+esc((resp[code]||{}).description||'')+'</span></div>';
961
980
  });
962
981
  html+='<div class="sec">Try it</div>';
963
- html+='<div class="try-field"><label class="try-label">x-api-key</label><input class="f" id="'+id+'_apikey" placeholder="API key (saved for this browser)" /></div>';
964
982
  pp.forEach(function(p){html+='<div class="try-field"><label class="try-label">'+esc(p.name)+' (path)</label><input class="f" data-kind="path" data-name="'+esc(p.name)+'" /></div>';});
965
983
  qp.forEach(function(p){html+='<div class="try-field"><label class="try-label">'+esc(p.name)+' (query)</label><input class="f" data-kind="query" data-name="'+esc(p.name)+'" /></div>';});
966
984
  if(e.op.security){html+='<div class="try-field"><label class="try-label">Authorization (Bearer token)</label><input class="f" id="'+id+'_auth" placeholder="token" /></div>';}
@@ -973,8 +991,6 @@ export function buildDocsHtml(specUrl = "openapi.json") {
973
991
 
974
992
  function wireTry(e,id,root){
975
993
  var btn=root.querySelector('#'+id+'_send');if(!btn)return;
976
- var keyEl=root.querySelector('#'+id+'_apikey');
977
- if(keyEl){ try{ keyEl.value=localStorage.getItem('docs_api_key')||''; }catch(err){} }
978
994
  // Auto-grow the body textarea to fit its content (capped, then scrolls).
979
995
  var bodyTa=root.querySelector('#'+id+'_body');
980
996
  if(bodyTa){
@@ -989,7 +1005,6 @@ export function buildDocsHtml(specUrl = "openapi.json") {
989
1005
  root.querySelectorAll('[data-kind=query]').forEach(function(inp){if(inp.value)qs.push(encodeURIComponent(inp.getAttribute('data-name'))+'='+encodeURIComponent(inp.value));});
990
1006
  if(qs.length)url+=(url.indexOf('?')>=0?'&':'?')+qs.join('&');
991
1007
  var opts={method:e.method.toUpperCase(),headers:{}};
992
- if(keyEl&&keyEl.value){ opts.headers['x-api-key']=keyEl.value; try{ localStorage.setItem('docs_api_key',keyEl.value); }catch(err){} }
993
1008
  var auth=root.querySelector('#'+id+'_auth');if(auth&&auth.value)opts.headers['Authorization']='Bearer '+auth.value;
994
1009
  var bodyEl=root.querySelector('#'+id+'_body');
995
1010
  if(bodyEl&&['post','put','patch'].indexOf(e.method)>=0){opts.headers['Content-Type']='application/json';opts.body=bodyEl.value;}
@@ -1023,7 +1038,11 @@ export function buildDocsHtml(specUrl = "openapi.json") {
1023
1038
  document.getElementById('apiVersion').textContent='v'+((spec.info&&spec.info.version)||'1.0.0');
1024
1039
  ITEMS=collect(spec);
1025
1040
  var nav=document.getElementById('nav');nav.innerHTML='';
1026
- if(!ITEMS.length){nav.innerHTML='<div class="group-title">No endpoints</div>';return;}
1041
+ if(!ITEMS.length){
1042
+ nav.innerHTML='<div class="group-title">No endpoints</div>';
1043
+ document.getElementById('main').innerHTML='<div class="main-empty">No endpoints in the spec yet. Add routes, then regenerate the docs from the dashboard (API Docs).</div>';
1044
+ return;
1045
+ }
1027
1046
  var groups={};ITEMS.forEach(function(e){var k=groupKey(e.path);(groups[k]=groups[k]||[]).push(e);});
1028
1047
  var first=null;
1029
1048
  Object.keys(groups).sort().forEach(function(k){
@@ -1046,9 +1065,20 @@ export function buildDocsHtml(specUrl = "openapi.json") {
1046
1065
  document.querySelectorAll('.nav-item').forEach(function(c){c.style.display=c.getAttribute('data-search').indexOf(q)>=0?'':'none';});
1047
1066
  });
1048
1067
 
1049
- fetch(SPEC_URL).then(function(r){return r.json();}).then(render).catch(function(){
1050
- document.getElementById('main').innerHTML='<div class="main-empty">Could not load the API spec.</div>';
1051
- });
1068
+ // The server inlines the spec below (so a protected page needs no second
1069
+ // authenticated request — avoids cross-browser cookie quirks on fetch). If it
1070
+ // isn't inlined (e.g. the page is served statically), fall back to fetching.
1071
+ var SPEC_DATA=/*__SPEC__*/null;
1072
+ if(SPEC_DATA&&typeof SPEC_DATA==='object'){
1073
+ render(SPEC_DATA);
1074
+ } else {
1075
+ fetch(SPEC_URL,{credentials:'same-origin'}).then(function(r){
1076
+ if(!r.ok){throw new Error('the server returned HTTP '+r.status+'. Regenerate the docs from the dashboard (API Docs).');}
1077
+ return r.json();
1078
+ }).then(render).catch(function(err){
1079
+ document.getElementById('main').innerHTML='<div class="main-empty">Could not load the API spec &mdash; '+esc(String((err&&err.message)||err))+'</div>';
1080
+ });
1081
+ }
1052
1082
  })();
1053
1083
  </script>
1054
1084
  </body>
@@ -1145,12 +1175,29 @@ router.use((req, res, next) => {
1145
1175
  // Interactive API documentation. The page is fully self-contained (no external
1146
1176
  // CDN); the spec is generated by 4bnode and served from this app.
1147
1177
  router.get('/openapi.json', (req, res) => {
1178
+ res.set('Cache-Control', 'no-store');
1148
1179
  if (!fs.existsSync(SPEC)) return res.status(404).json({ error: 'API spec not generated yet.' });
1149
1180
  res.sendFile(SPEC);
1150
1181
  });
1151
1182
 
1183
+ const DOCS_PAGE = ${JSON.stringify(buildDocsHtml("/docs/openapi.json"))};
1184
+
1152
1185
  router.get('/', (req, res) => {
1153
- res.type('html').send(${JSON.stringify(buildDocsHtml("/docs/openapi.json"))});
1186
+ // Inline the spec into the page so a password-protected page doesn't need a
1187
+ // second authenticated request (some browsers won't attach the docs cookie to
1188
+ // the fetch). Falls back to fetching /openapi.json if no spec is inlined.
1189
+ let spec = 'null';
1190
+ try {
1191
+ if (fs.existsSync(SPEC)) {
1192
+ const raw = fs.readFileSync(SPEC, 'utf8').trim();
1193
+ // Escape '<' so a spec value containing '</script>' can't break out of the
1194
+ // inline <script>. The '\\u003c' sequence still evaluates to '<' in JS.
1195
+ if (raw) spec = raw.split('<').join(String.fromCharCode(92) + 'u003c');
1196
+ }
1197
+ } catch {}
1198
+ // Never cache the docs page — keeps it from going stale after a regenerate.
1199
+ res.set('Cache-Control', 'no-store');
1200
+ res.type('html').send(DOCS_PAGE.replace('/*__SPEC__*/null', () => spec));
1154
1201
  });
1155
1202
 
1156
1203
  export default router;
@@ -21,10 +21,11 @@ import {
21
21
  generateMongooseModel,
22
22
  buildErrorHandlerFile,
23
23
  buildRolesGuardFile,
24
- buildApiKeyFile,
25
24
  buildValidateFile,
26
25
  buildZodSchemaFile,
27
26
  wireValidateIntoRouteContent,
27
+ buildAuthMiddleware,
28
+ wireAuthIntoRouteContent,
28
29
  securityWiring,
29
30
  addImportAfterLastImport,
30
31
  insertBeforeListenContent,
@@ -139,7 +140,8 @@ test("securityWiring composes imports + insertion blocks", () => {
139
140
  assert.ok(w.imports.includes("import helmet from 'helmet';"));
140
141
  assert.ok(w.imports.includes("import rateLimit from 'express-rate-limit';"));
141
142
  assert.ok(w.imports.some((i) => i.includes("errorHandler.js")));
142
- assert.ok(w.beforeRoutes.includes("app.use(helmet())"));
143
+ assert.ok(w.beforeRoutes.includes("app.use(helmet("));
144
+ assert.ok(w.beforeRoutes.includes("contentSecurityPolicy:"));
143
145
  assert.ok(w.beforeRoutes.includes("rateLimit({"));
144
146
  assert.ok(w.beforeListen.includes("app.use(notFound)"));
145
147
  assert.ok(w.beforeListen.includes("app.use(errorHandler)"));
@@ -155,7 +157,6 @@ test("securityWiring only includes selected features", () => {
155
157
  test("middleware file builders produce valid-looking modules", () => {
156
158
  assert.ok(buildErrorHandlerFile().includes("export function errorHandler"));
157
159
  assert.ok(buildRolesGuardFile().includes("export default roles"));
158
- assert.ok(buildApiKeyFile().includes("req.header('x-api-key')"));
159
160
  assert.ok(buildValidateFile().includes("schema.safeParse"));
160
161
  });
161
162
 
@@ -197,6 +198,25 @@ test("wireValidateIntoRouteContent guards write endpoints only, idempotently", (
197
198
  assert.equal(wireValidateIntoRouteContent(out, "user"), out, "idempotent");
198
199
  });
199
200
 
201
+ test("buildAuthMiddleware + wireAuthIntoRouteContent protects every endpoint, auth-first, idempotently", () => {
202
+ assert.ok(buildAuthMiddleware().includes("jwt.verify(token, process.env.JWT_SECRET)"));
203
+ assert.ok(buildAuthMiddleware().includes("export default auth"));
204
+ const route = [
205
+ "import express from 'express';",
206
+ "const router = express.Router();",
207
+ "router.get('/', async (req, res) => {});",
208
+ "router.post('/', validate(userSchema), async (req, res) => {});",
209
+ "router.put('/:id', upload.single('avatar'), async (req, res) => {});",
210
+ "export default router;",
211
+ ].join("\n");
212
+ const out = wireAuthIntoRouteContent(route);
213
+ assert.ok(out.includes("import auth from '../middleware/auth.js';"));
214
+ assert.ok(out.includes("router.get('/', auth, async (req, res)"), "GET protected");
215
+ assert.ok(out.includes("router.post('/', auth, validate(userSchema), async (req, res)"), "auth before validate");
216
+ assert.ok(out.includes("router.put('/:id', auth, upload.single('avatar'), async (req, res)"), "auth before multer");
217
+ assert.equal(wireAuthIntoRouteContent(out), out, "idempotent");
218
+ });
219
+
200
220
  // ── Pure index.js transforms (shared by CLI + dashboard) ────────────────────
201
221
  test("addImportAfterLastImport inserts after last import, idempotently", () => {
202
222
  const base = "import a from 'a';\nimport b from 'b';\n\nconst x = 1;\n";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "4bnode",
3
- "version": "4.1.2",
3
+ "version": "4.1.4",
4
4
  "description": "4bnode is a CLI-powered backend development platform with a built-in visual dashboard to generate, manage, and test Node.js/Express APIs faster.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -12,7 +12,6 @@ import { RoutesPage } from "./pages/RoutesPage.js";
12
12
  import { DocsPage } from "./pages/DocsPage.js";
13
13
  import { AiBuilderPage } from "./pages/AiBuilderPage.js";
14
14
  import { SecurityPage } from "./pages/SecurityPage.js";
15
- import { ApiKeysPage } from "./pages/ApiKeysPage.js";
16
15
  import { ApiTesterPage } from "./pages/ApiTesterPage.js";
17
16
  import { RealtimePage } from "./pages/RealtimePage.js";
18
17
  import { SerialPortPage } from "./pages/SerialPortPage.js";
@@ -100,7 +99,6 @@ export function App() {
100
99
  routes: html`<${RoutesPage} />`,
101
100
  docs: html`<${DocsPage} />`,
102
101
  security: html`<${SecurityPage} />`,
103
- "api-keys": html`<${ApiKeysPage} />`,
104
102
  "api-tester": html`<${ApiTesterPage} />`,
105
103
  realtime: html`<${RealtimePage} />`,
106
104
  serialport: html`<${SerialPortPage} />`,