4bnode 4.1.0 → 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 +46 -5
  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/lib/ui.js CHANGED
@@ -1,6 +1,9 @@
1
+ import fs from "fs";
2
+ import path from "path";
1
3
  import chalk from "chalk";
2
4
  import gradient from "gradient-string";
3
5
  import ora from "ora";
6
+ import { addDepsToPackageJson } from "./codegen.js";
4
7
 
5
8
  // Brand gradients
6
9
  const brand = gradient(["#00c6ff", "#0072ff", "#7b2ff7"]);
@@ -132,14 +135,24 @@ export function showNextSteps(steps) {
132
135
 
133
136
  export async function installDeps(packages, opts = {}) {
134
137
  const { execSync } = await import("child_process");
138
+ const { dev = false, ...execOpts } = opts;
135
139
  const names = Array.isArray(packages) ? packages : [packages];
136
140
  const label = names.join(", ");
141
+ // Record in package.json first, so the app is installable even if the live
142
+ // install fails (offline) — a later `npm install` then resolves everything.
143
+ try {
144
+ const pkgPath = path.join(execOpts.cwd || process.cwd(), "package.json");
145
+ if (fs.existsSync(pkgPath)) {
146
+ const { content, changed } = addDepsToPackageJson(fs.readFileSync(pkgPath, "utf8"), names, { dev });
147
+ if (changed) fs.writeFileSync(pkgPath, content, "utf8");
148
+ }
149
+ } catch {}
137
150
  const spinner = createSpinner(`Installing ${label}...`);
138
151
  spinner.start();
139
152
  try {
140
- execSync(`npm install ${names.join(" ")}`, {
153
+ execSync(`npm install ${dev ? "-D " : ""}${names.join(" ")}`, {
141
154
  stdio: "pipe",
142
- ...opts,
155
+ ...execOpts,
143
156
  });
144
157
  spinner.succeed(success(`Installed ${label}`));
145
158
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "4bnode",
3
- "version": "4.1.0",
3
+ "version": "4.1.1",
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",
@@ -8,7 +8,9 @@
8
8
  "4bnode": "index.js"
9
9
  },
10
10
  "scripts": {
11
- "test": "echo \"Error: no test specified\" && exit 1"
11
+ "test": "node lib/codegen.test.js",
12
+ "sync": "node lib/sync-skeleton.js",
13
+ "prepublishOnly": "npm run sync"
12
14
  },
13
15
  "repository": {
14
16
  "type": "git",
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env node
2
+ // Audit the /_dev dashboard exposure across 4bnode apps (read-only).
3
+ //
4
+ // node scripts/audit-dev-surface.mjs <dir> [--since=2026-06-07]
5
+ //
6
+ // Scans <dir> recursively for 4bnode apps (any folder containing
7
+ // .4bnode/dev-api.js) and reports, per app:
8
+ // • whether the /_dev mount is gated on NODE_ENV (production-safe)
9
+ // • whether dev-api.js carries the loopback network guard
10
+ // • whether a .passkey exists and whether it is a known-weak PIN
11
+ // • route/source files modified on/after --since, and any that contain
12
+ // code-execution / cryptominer signatures (possible injected backdoor)
13
+ //
14
+ // Nothing is modified. Rotate weak/shared passkeys with `npx 4bnode passkey`.
15
+
16
+ import fs from 'fs';
17
+ import path from 'path';
18
+ import crypto from 'crypto';
19
+
20
+ const args = process.argv.slice(2);
21
+ const root = path.resolve(args.find((a) => !a.startsWith('--')) || '.');
22
+ const sinceArg = (args.find((a) => a.startsWith('--since=')) || '').split('=')[1];
23
+ const since = sinceArg ? new Date(sinceArg) : null;
24
+
25
+ const sha = (s) => crypto.createHash('sha256').update(s).digest('hex');
26
+ // Common weak PINs operators reuse. Hashes compared against .4bnode/.passkey.
27
+ const WEAK_PINS = ['123456', '000000', '111111', '121212', '123123', '654321',
28
+ '666666', '999999', '112233', '159753', '147258', '101010', '696969', '777777',
29
+ '123321', '789456', '000001', '222222', '555555', '888888', '123654', '098765'];
30
+ const WEAK_HASHES = new Map(WEAK_PINS.map((p) => [sha(p), p]));
31
+
32
+ // Signatures of code execution / miners that should never appear in generated routes.
33
+ const BACKDOOR_PATTERNS = [
34
+ [/child_process|\brequire\(\s*['"]child_process['"]\s*\)/, 'child_process import'],
35
+ [/\bexec(Sync)?\s*\(/, 'exec() call'],
36
+ [/\bspawn(Sync)?\s*\(/, 'spawn() call'],
37
+ [/\beval\s*\(/, 'eval()'],
38
+ [/new\s+Function\s*\(/, 'new Function()'],
39
+ [/Buffer\.from\([^)]*,\s*['"]base64['"]\)/, 'base64-decoded payload'],
40
+ [/(curl|wget)\s+[^\n|]*\|\s*(sh|bash)/, 'pipe-to-shell download'],
41
+ [/xmrig|minerd|stratum\+tcp|coinhive|cryptonight|nanopool|supportxmr|nicehash/i, 'cryptominer signature'],
42
+ [/\/dev\/tcp\//, 'reverse-shell /dev/tcp'],
43
+ ];
44
+
45
+ const C = { red: (s) => `\x1b[31m${s}\x1b[0m`, yel: (s) => `\x1b[33m${s}\x1b[0m`,
46
+ grn: (s) => `\x1b[32m${s}\x1b[0m`, dim: (s) => `\x1b[2m${s}\x1b[0m`, bold: (s) => `\x1b[1m${s}\x1b[0m` };
47
+
48
+ function findApps(dir, depth = 0, acc = []) {
49
+ if (depth > 4) return acc;
50
+ let entries;
51
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return acc; }
52
+ if (entries.some((e) => e.isDirectory() && e.name === '.4bnode') &&
53
+ fs.existsSync(path.join(dir, '.4bnode', 'dev-api.js'))) {
54
+ acc.push(dir);
55
+ }
56
+ for (const e of entries) {
57
+ if (e.isDirectory() && e.name !== 'node_modules' && e.name !== '.git' && e.name !== '.4bnode') {
58
+ findApps(path.join(dir, e.name), depth + 1, acc);
59
+ }
60
+ }
61
+ return acc;
62
+ }
63
+
64
+ function scanSourceFiles(appDir) {
65
+ const targets = ['index.js', 'src'];
66
+ const files = [];
67
+ const walk = (p) => {
68
+ let st; try { st = fs.statSync(p); } catch { return; }
69
+ if (st.isDirectory()) {
70
+ if (path.basename(p) === 'node_modules' || path.basename(p) === '.git') return;
71
+ for (const c of fs.readdirSync(p)) walk(path.join(p, c));
72
+ } else if (p.endsWith('.js') || p.endsWith('.mjs') || p.endsWith('.cjs')) {
73
+ files.push(p);
74
+ }
75
+ };
76
+ for (const t of targets) walk(path.join(appDir, t));
77
+ const findings = [];
78
+ for (const f of files) {
79
+ let content, mtime;
80
+ try { content = fs.readFileSync(f, 'utf8'); mtime = fs.statSync(f).mtime; } catch { continue; }
81
+ const rel = path.relative(appDir, f);
82
+ const hits = [];
83
+ const lines = content.split('\n');
84
+ for (const [re, label] of BACKDOOR_PATTERNS) {
85
+ for (let i = 0; i < lines.length; i++) {
86
+ if (re.test(lines[i])) hits.push(`${rel}:${i + 1} — ${label}`);
87
+ }
88
+ }
89
+ const recent = since && mtime >= since;
90
+ if (hits.length || recent) findings.push({ rel, mtime, hits, recent });
91
+ }
92
+ return findings;
93
+ }
94
+
95
+ function auditApp(appDir) {
96
+ const name = path.basename(appDir);
97
+ const issues = [];
98
+ // 1. Mount gating
99
+ const idx = path.join(appDir, 'index.js');
100
+ if (fs.existsSync(idx)) {
101
+ const c = fs.readFileSync(idx, 'utf8');
102
+ const mountIdx = Math.max(c.indexOf('app.use("/_dev"'), c.indexOf("app.use('/_dev'"));
103
+ if (mountIdx !== -1) {
104
+ const gateIdx = c.search(/NODE_ENV\s*!==\s*['"]production['"]/);
105
+ const gated = gateIdx !== -1 && gateIdx < mountIdx;
106
+ if (!gated) issues.push([C.red('CRITICAL'), '/_dev is mounted WITHOUT a NODE_ENV production gate']);
107
+ }
108
+ }
109
+ // 2. Loopback guard in dev-api.js
110
+ const devApi = path.join(appDir, '.4bnode', 'dev-api.js');
111
+ const devApiSrc = fs.existsSync(devApi) ? fs.readFileSync(devApi, 'utf8') : '';
112
+ if (devApiSrc && !devApiSrc.includes('isLoopbackRequest')) {
113
+ issues.push([C.red('CRITICAL'), 'dev-api.js has NO loopback network guard (reachable off-host)']);
114
+ }
115
+ // 3. Passkey
116
+ const pkFile = path.join(appDir, '.4bnode', '.passkey');
117
+ if (!fs.existsSync(pkFile)) {
118
+ issues.push([C.red('CRITICAL'), 'NO .passkey file — dashboard auth is open (anyone gets in)']);
119
+ } else {
120
+ const h = fs.readFileSync(pkFile, 'utf8').trim();
121
+ if (WEAK_HASHES.has(h)) {
122
+ issues.push([C.red('CRITICAL'), `weak/guessable passkey "${WEAK_HASHES.get(h)}" — rotate with \`npx 4bnode passkey\``]);
123
+ }
124
+ }
125
+ // 4. Backdoor / recent-change scan
126
+ const findings = scanSourceFiles(appDir);
127
+ const backdoors = findings.filter((f) => f.hits.length);
128
+ const recents = findings.filter((f) => f.recent && !f.hits.length);
129
+
130
+ console.log(`\n${C.bold(name)} ${C.dim(appDir)}`);
131
+ if (!issues.length && !backdoors.length) {
132
+ console.log(` ${C.grn('✔')} no exposure issues found`);
133
+ }
134
+ for (const [sev, msg] of issues) console.log(` ${sev} ${msg}`);
135
+ for (const f of backdoors) {
136
+ console.log(` ${C.red('BACKDOOR?')} ${f.rel} ${C.dim('(modified ' + f.mtime.toISOString() + ')')}`);
137
+ for (const h of f.hits) console.log(` ${C.yel(h)}`);
138
+ }
139
+ if (recents.length) {
140
+ console.log(` ${C.yel('REVIEW')} ${recents.length} source file(s) modified since ${sinceArg}:`);
141
+ for (const f of recents) console.log(` ${f.rel} ${C.dim(f.mtime.toISOString())}`);
142
+ }
143
+ return { name, critical: issues.filter((i) => i[0].includes('CRITICAL')).length + backdoors.length };
144
+ }
145
+
146
+ console.log(C.bold(`\n4bnode /_dev exposure audit — ${root}`));
147
+ if (since) console.log(C.dim(`Flagging source changed on/after ${sinceArg}`));
148
+ const apps = findApps(root);
149
+ if (!apps.length) { console.log(C.yel('\nNo 4bnode apps (.4bnode/dev-api.js) found under that path.')); process.exit(0); }
150
+ let totalCritical = 0;
151
+ for (const app of apps) totalCritical += auditApp(app).critical;
152
+ console.log(`\n${C.bold('Summary:')} ${apps.length} app(s) scanned, ${totalCritical ? C.red(totalCritical + ' critical finding(s)') : C.grn('0 critical findings')}.`);
153
+ process.exit(totalCritical ? 1 : 0);
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+ // Reveal the 6-digit dashboard PIN for a 4bnode app by reversing its hash.
3
+ //
4
+ // node scripts/reveal-passkey.mjs /path/to/app
5
+ //
6
+ // The PIN is stored as a SHA-256 hash in .4bnode/.passkey. A 6-digit PIN has
7
+ // only 1,000,000 possibilities, so we just hash them all and match. (This is
8
+ // exactly why a 6-digit PIN is weak — but it lets you recover/audit it.)
9
+
10
+ import fs from 'fs';
11
+ import path from 'path';
12
+ import crypto from 'crypto';
13
+
14
+ const appDir = path.resolve(process.argv[2] || '.');
15
+ const pkFile = path.join(appDir, '.4bnode', '.passkey');
16
+
17
+ if (!fs.existsSync(pkFile)) {
18
+ console.log(`No .passkey file in ${path.join(appDir, '.4bnode')} — the dashboard had NO password set (open).`);
19
+ process.exit(0);
20
+ }
21
+
22
+ const target = fs.readFileSync(pkFile, 'utf8').trim();
23
+ const sha = (s) => crypto.createHash('sha256').update(s).digest('hex');
24
+
25
+ // Try all 6-digit PINs (000000–999999), including shorter numeric values.
26
+ for (let n = 0; n < 1_000_000; n++) {
27
+ const pin = String(n).padStart(6, '0');
28
+ if (sha(pin) === target) { console.log(`PIN: ${pin}`); process.exit(0); }
29
+ }
30
+ // Fallback: also try the value un-padded (e.g. "1234") in case it wasn't 6 digits.
31
+ for (let n = 0; n < 1_000_000; n++) {
32
+ if (sha(String(n)) === target) { console.log(`PIN: ${n}`); process.exit(0); }
33
+ }
34
+ console.log('Not a numeric PIN up to 6 digits — the passkey is something else (longer/alphanumeric).');
35
+ console.log(`Stored hash: ${target}`);
@@ -9,11 +9,15 @@ import { DashboardPage } from "./pages/DashboardPage.js";
9
9
  import { DatabasePage } from "./pages/DatabasePage.js";
10
10
  import { SchemasPage } from "./pages/SchemasPage.js";
11
11
  import { RoutesPage } from "./pages/RoutesPage.js";
12
+ import { DocsPage } from "./pages/DocsPage.js";
13
+ import { AiBuilderPage } from "./pages/AiBuilderPage.js";
14
+ import { SecurityPage } from "./pages/SecurityPage.js";
15
+ import { ApiKeysPage } from "./pages/ApiKeysPage.js";
12
16
  import { ApiTesterPage } from "./pages/ApiTesterPage.js";
13
17
  import { RealtimePage } from "./pages/RealtimePage.js";
14
18
  import { SerialPortPage } from "./pages/SerialPortPage.js";
19
+ import { MailPage } from "./pages/MailPage.js";
15
20
  import { LogsPage } from "./pages/LogsPage.js";
16
- import { ReportsPage } from "./pages/ReportsPage.js";
17
21
  import { EnvironmentPage } from "./pages/EnvironmentPage.js";
18
22
 
19
23
  function getHashPage() {
@@ -61,12 +65,27 @@ export function App() {
61
65
  }, []);
62
66
 
63
67
  const refresh = useCallback(async () => {
64
- const [dashData, statusData] = await Promise.all([
65
- api("/dashboard"),
66
- api("/status"),
67
- ]);
68
- setData(dashData);
69
- setStatus(statusData);
68
+ // File-writing actions (create route, docs, security, ...) modify index.js,
69
+ // which triggers a nodemon restart. Since the dashboard is served by that same
70
+ // app, the refresh fetch fired right after can hit the restart window and fail,
71
+ // leaving the UI stale until a manual reload. Retry across the restart so new
72
+ // state shows up in real time.
73
+ let lastErr;
74
+ for (let attempt = 0; attempt < 8; attempt++) {
75
+ try {
76
+ const [dashData, statusData] = await Promise.all([
77
+ api("/dashboard"),
78
+ api("/status"),
79
+ ]);
80
+ setData(dashData);
81
+ setStatus(statusData);
82
+ return;
83
+ } catch (err) {
84
+ lastErr = err;
85
+ await new Promise((r) => setTimeout(r, 500));
86
+ }
87
+ }
88
+ throw lastErr;
70
89
  }, []);
71
90
 
72
91
  useEffect(() => {
@@ -75,13 +94,17 @@ export function App() {
75
94
 
76
95
  const pages = {
77
96
  dashboard: html`<${DashboardPage} />`,
97
+ "ai-builder": html`<${AiBuilderPage} />`,
78
98
  database: html`<${DatabasePage} />`,
79
99
  schemas: html`<${SchemasPage} />`,
80
100
  routes: html`<${RoutesPage} />`,
101
+ docs: html`<${DocsPage} />`,
102
+ security: html`<${SecurityPage} />`,
103
+ "api-keys": html`<${ApiKeysPage} />`,
81
104
  "api-tester": html`<${ApiTesterPage} />`,
82
105
  realtime: html`<${RealtimePage} />`,
83
106
  serialport: html`<${SerialPortPage} />`,
84
- reports: html`<${ReportsPage} />`,
107
+ mail: html`<${MailPage} />`,
85
108
  logs: html`<${LogsPage} />`,
86
109
  env: html`<${EnvironmentPage} />`,
87
110
  };
@@ -93,7 +116,6 @@ export function App() {
93
116
  <div className="main">
94
117
  <div className="topbar">
95
118
  <h2>${PAGE_TITLES[page] || "Dashboard"}</h2>
96
- <span className="topbar-badge">Development</span>
97
119
  </div>
98
120
  <div className="content">
99
121
  <${AnimatePresence} mode="wait">
@@ -3,11 +3,9 @@ import { AnimatePresence } from "framer-motion";
3
3
  import { motion } from "framer-motion";
4
4
  import { html, API, getStoredPasskey } from "./lib.js";
5
5
  import { App } from "./App.js";
6
- import { SplashScreen } from "./screens/SplashScreen.js";
7
6
  import { PasskeyScreen } from "./screens/PasskeyScreen.js";
8
7
 
9
8
  export function Root() {
10
- const [showSplash, setShowSplash] = useState(true);
11
9
  const [authed, setAuthed] = useState(false);
12
10
  const [checking, setChecking] = useState(true);
13
11
 
@@ -29,9 +27,7 @@ export function Root() {
29
27
  if (checking) return null;
30
28
 
31
29
  return html`<${AnimatePresence} mode="wait">
32
- ${showSplash
33
- ? html`<${SplashScreen} key="splash" onDone=${() => setShowSplash(false)} />`
34
- : !authed
30
+ ${!authed
35
31
  ? html`<${PasskeyScreen} key="passkey" onSuccess=${() => setAuthed(true)} />`
36
32
  : html`<${motion.div} key="app"
37
33
  style=${{ display: 'flex', height: '100vh', width: '100%' }}
@@ -13,6 +13,8 @@ export function Sidebar() {
13
13
  icon: Icons.dashboard,
14
14
  section: "Overview",
15
15
  },
16
+ // AI Builder — temporarily hidden from the menu (re-enable by uncommenting)
17
+ // { id: "ai-builder", label: "AI Builder", icon: Icons.ai },
16
18
  {
17
19
  id: "database",
18
20
  label: "Database",
@@ -27,15 +29,18 @@ export function Sidebar() {
27
29
  lockMsg: "Requires database",
28
30
  },
29
31
  { id: "routes", label: "API Routes", icon: Icons.route },
32
+ { id: "docs", label: "API Docs", icon: Icons.docs },
30
33
  { id: "api-tester", label: "API Tester", icon: Icons.apiTester },
31
34
  {
32
- id: "realtime",
33
- label: "Real-time",
34
- icon: Icons.realtime,
35
+ id: "security",
36
+ label: "Security",
37
+ icon: Icons.shield,
35
38
  section: "Features",
36
39
  },
40
+ { id: "api-keys", label: "API Keys", icon: Icons.apiKeys },
41
+ { id: "realtime", label: "Real-time", icon: Icons.realtime },
37
42
  { id: "serialport", label: "SerialPort", icon: Icons.serial },
38
- { id: "reports", label: "Reports", icon: Icons.reports, locked: !dbOk, lockMsg: "Requires database" },
43
+ { id: "mail", label: "Email", icon: Icons.mail },
39
44
  { id: "logs", label: "Logs", icon: Icons.logs, section: "Settings" },
40
45
  { id: "env", label: "Environment", icon: Icons.env },
41
46
  ];
@@ -44,7 +49,6 @@ export function Sidebar() {
44
49
  return html`<div className="sidebar">
45
50
  <div className="sidebar-brand">
46
51
  <pre>${`██╗ ██╗██████╗ ███╗ ██╗ ██████╗ ██████╗ ███████╗\n██║ ██║██╔══██╗████╗ ██║██╔═══██╗██╔══██╗██╔════╝\n███████║██████╔╝██╔██╗ ██║██║ ██║██║ ██║█████╗\n╚════██║██╔══██╗██║╚██╗██║██║ ██║██║ ██║██╔══╝\n ██║██████╔╝██║ ╚████║╚██████╔╝██████╔╝███████╗\n ╚═╝╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚══════╝`}</pre>
47
- <span>Project Dashboard</span>
48
52
  </div>
49
53
  ${navItems.map((item) => {
50
54
  const sectionHeader =
@@ -20,7 +20,10 @@ import {
20
20
  LuSend,
21
21
  LuSettings,
22
22
  LuScrollText,
23
- LuFileSpreadsheet,
23
+ LuSparkles,
24
+ LuBookOpen,
25
+ LuMail,
26
+ LuKeyRound,
24
27
  } from "react-icons/lu";
25
28
 
26
29
  export const html = htm.bind(React.createElement);
@@ -61,8 +64,11 @@ export async function api(path, options = {}) {
61
64
  if (res.ok) return { message: "Done" };
62
65
  throw new Error("Server restarting, please wait...");
63
66
  }
64
- if (!res.ok)
65
- throw new Error(data.error || data.message || "Request failed");
67
+ if (!res.ok) {
68
+ const e = new Error(data.error || data.message || "Request failed");
69
+ e.data = data; // expose extra fields (e.g. command output) to callers
70
+ throw e;
71
+ }
66
72
  return data;
67
73
  }
68
74
 
@@ -82,22 +88,29 @@ export const Icons = {
82
88
  serial: html`<${LuCable} size=${16} />`,
83
89
  zap: html`<${LuZap} size=${16} />`,
84
90
  shield: html`<${LuShield} size=${16} />`,
91
+ apiKeys: html`<${LuKeyRound} size=${16} />`,
85
92
  box: html`<${LuBox} size=${16} />`,
86
93
  apiTester: html`<${LuFlaskConical} size=${16} />`,
87
94
  send: html`<${LuSend} size=${16} />`,
88
95
  logs: html`<${LuScrollText} size=${16} />`,
89
- reports: html`<${LuFileSpreadsheet} size=${16} />`,
96
+ ai: html`<${LuSparkles} size=${16} />`,
97
+ docs: html`<${LuBookOpen} size=${16} />`,
98
+ mail: html`<${LuMail} size=${16} />`,
90
99
  };
91
100
 
92
101
  export const PAGE_TITLES = {
93
102
  dashboard: "Dashboard",
103
+ "ai-builder": "AI Builder",
94
104
  database: "Database",
95
105
  schemas: "Schemas",
96
106
  routes: "API Routes",
107
+ docs: "API Docs",
108
+ security: "Security",
109
+ "api-keys": "API Keys",
97
110
  "api-tester": "API Tester",
98
111
  realtime: "Real-time",
99
112
  serialport: "SerialPort",
113
+ mail: "Email",
100
114
  logs: "Logs",
101
- reports: "Reports",
102
115
  env: "Environment",
103
116
  };