4bnode 4.1.4 → 4.1.6

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 (70) hide show
  1. package/add-api.js +5 -3
  2. package/add-docs.js +18 -5
  3. package/add-email.js +118 -28
  4. package/add-login.js +35 -16
  5. package/add-mongo-schema.js +13 -5
  6. package/lib/codegen.js +408 -104
  7. package/lib/codegen.test.js +122 -15
  8. package/lib/names.js +1 -1
  9. package/package.json +1 -1
  10. package/skeleton/.4bnode/assets/js/components/Sidebar.js +2 -2
  11. package/skeleton/.4bnode/assets/js/pages/ApiTesterPage.js +54 -9
  12. package/skeleton/.4bnode/assets/js/pages/LogsPage.js +49 -16
  13. package/skeleton/.4bnode/assets/js/pages/MailPage.js +221 -71
  14. package/skeleton/.4bnode/assets/js/pages/RoutesPage.js +51 -2
  15. package/skeleton/.4bnode/assets/js/pages/SecurityPage.js +1 -1
  16. package/skeleton/.4bnode/codegen.js +408 -104
  17. package/skeleton/.4bnode/dev-api.js +384 -30
  18. package/skeleton/.env +6 -0
  19. package/skeleton/.env.example +25 -0
  20. package/skeleton/_gitignore +1 -0
  21. package/skeleton/index.js +7 -2
  22. package/skeleton/src/middleware/apiKey.js +14 -1
  23. package/skeleton/src/middleware/errorHandler.js +7 -2
  24. package/app-1/.4bnode/.passkey +0 -1
  25. package/app-1/.4bnode/assets/fonts/NotoSans-Bold.woff2 +0 -0
  26. package/app-1/.4bnode/assets/fonts/NotoSans-Light.woff2 +0 -0
  27. package/app-1/.4bnode/assets/fonts/NotoSans-Medium.woff2 +0 -0
  28. package/app-1/.4bnode/assets/fonts/NotoSans-Regular.woff2 +0 -0
  29. package/app-1/.4bnode/assets/fonts/NotoSans-SemiBold.woff2 +0 -0
  30. package/app-1/.4bnode/assets/images/mongo.png +0 -0
  31. package/app-1/.4bnode/assets/js/App.js +0 -139
  32. package/app-1/.4bnode/assets/js/Root.js +0 -40
  33. package/app-1/.4bnode/assets/js/components/Modal.js +0 -52
  34. package/app-1/.4bnode/assets/js/components/Sidebar.js +0 -73
  35. package/app-1/.4bnode/assets/js/components/ToastContainer.js +0 -15
  36. package/app-1/.4bnode/assets/js/context.js +0 -7
  37. package/app-1/.4bnode/assets/js/lib.js +0 -113
  38. package/app-1/.4bnode/assets/js/main.js +0 -5
  39. package/app-1/.4bnode/assets/js/pages/AiBuilderPage.js +0 -268
  40. package/app-1/.4bnode/assets/js/pages/ApiTesterPage.js +0 -1879
  41. package/app-1/.4bnode/assets/js/pages/DashboardPage.js +0 -435
  42. package/app-1/.4bnode/assets/js/pages/DatabasePage.js +0 -300
  43. package/app-1/.4bnode/assets/js/pages/DocsPage.js +0 -150
  44. package/app-1/.4bnode/assets/js/pages/EnvironmentPage.js +0 -103
  45. package/app-1/.4bnode/assets/js/pages/LogsPage.js +0 -205
  46. package/app-1/.4bnode/assets/js/pages/MailPage.js +0 -134
  47. package/app-1/.4bnode/assets/js/pages/RealtimePage.js +0 -186
  48. package/app-1/.4bnode/assets/js/pages/RoutesPage.js +0 -1436
  49. package/app-1/.4bnode/assets/js/pages/SchemasPage.js +0 -838
  50. package/app-1/.4bnode/assets/js/pages/SecurityPage.js +0 -74
  51. package/app-1/.4bnode/assets/js/pages/SerialPortPage.js +0 -170
  52. package/app-1/.4bnode/assets/js/screens/PasskeyScreen.js +0 -105
  53. package/app-1/.4bnode/codegen.js +0 -1249
  54. package/app-1/.4bnode/dev-api.js +0 -3621
  55. package/app-1/.4bnode/ui.html +0 -1578
  56. package/app-1/README.md +0 -47
  57. package/app-1/ecosystem.config.cjs +0 -25
  58. package/app-1/index.js +0 -145
  59. package/app-1/package.json +0 -35
  60. package/app-1/public/index.html +0 -292
  61. package/app-1/src/db.js +0 -13
  62. package/app-1/src/docs.js +0 -84
  63. package/app-1/src/middleware/errorHandler.js +0 -15
  64. package/app-1/src/middleware/roles.js +0 -14
  65. package/app-1/src/middleware/validate.js +0 -20
  66. package/app-1/src/models/login.js +0 -9
  67. package/app-1/src/openapi.json +0 -62
  68. package/app-1/src/routes/products.js +0 -26
  69. package/app-1/src/validators/products.js +0 -7
  70. package/app-1/src/validators/user.js +0 -7
package/skeleton/index.js CHANGED
@@ -23,13 +23,15 @@ const __dirname = path.resolve();
23
23
  // ── Middleware ───────────────────────────────────────
24
24
  app.use(express.json({ limit: "50mb" }));
25
25
  app.use(express.urlencoded({ extended: true, limit: "50mb" }));
26
- app.use(cors({ origin: "*" }));
27
26
 
28
27
  // ── Dev Dashboard (local development only) ───────────
29
28
  // The dashboard at /_dev can write files and run shell commands — it is a
30
29
  // remote-code-execution surface and must NEVER be reachable in production.
31
30
  // In production it is not even imported. It additionally restricts itself to
32
- // loopback requests and requires a passkey (see .4bnode/dev-api.js).
31
+ // loopback requests, validates Host/Origin (defeats DNS-rebinding), and requires
32
+ // a passkey for remote access (see .4bnode/dev-api.js). It is mounted BEFORE the
33
+ // permissive global CORS below so `Access-Control-Allow-Origin: *` never applies
34
+ // to the dashboard.
33
35
  // • production (NODE_ENV=production): OFF — set _4BNODE_DASHBOARD=on to force.
34
36
  // • non-production: ON — set _4BNODE_DASHBOARD=off to disable.
35
37
  const dashboardEnabled =
@@ -42,6 +44,9 @@ if (dashboardEnabled) {
42
44
  } catch {}
43
45
  }
44
46
 
47
+ // ── CORS (applies to your API, not the /_dev dashboard above) ──
48
+ app.use(cors({ origin: "*" }));
49
+
45
50
  // ── Security (built-in by default) ───────────────────
46
51
  // Mounted AFTER the /_dev dashboard so its CDN/inline scripts aren't blocked by
47
52
  // helmet's CSP and the dashboard isn't throttled by the rate limiter.
@@ -4,6 +4,7 @@
4
4
  // app.use('/api/private', apiKey);
5
5
  import fs from 'fs';
6
6
  import path from 'path';
7
+ import crypto from 'crypto';
7
8
  import { fileURLToPath } from 'url';
8
9
 
9
10
  const KEYS_FILE = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'api-keys.json');
@@ -24,10 +25,22 @@ function loadKeys() {
24
25
  return _cache.keys;
25
26
  }
26
27
 
28
+ // Constant-time membership test: hash both sides to a fixed length so neither the
29
+ // match result nor the key length leaks through string-comparison timing.
30
+ function keyMatches(provided, valid) {
31
+ const p = crypto.createHash('sha256').update(String(provided)).digest();
32
+ let ok = false;
33
+ for (const k of valid) {
34
+ const h = crypto.createHash('sha256').update(String(k)).digest();
35
+ if (crypto.timingSafeEqual(p, h)) ok = true; // no early return — keep timing flat
36
+ }
37
+ return ok;
38
+ }
39
+
27
40
  const apiKey = (req, res, next) => {
28
41
  const provided = req.header('x-api-key');
29
42
  const valid = loadKeys();
30
- if (!provided || !valid.includes(provided)) {
43
+ if (!provided || !keyMatches(provided, valid)) {
31
44
  return res.status(401).json({ message: 'Invalid or missing API key' });
32
45
  }
33
46
  next();
@@ -8,8 +8,13 @@ export function notFound(req, res, next) {
8
8
  export function errorHandler(err, req, res, next) {
9
9
  const status = err.status || err.statusCode || 500;
10
10
  console.error(err);
11
+ const isProd = process.env.NODE_ENV === 'production';
12
+ // In production, only surface messages for client (4xx) errors or errors
13
+ // explicitly marked safe (err.expose). Never leak raw 5xx internals — a Mongo
14
+ // E11000 or driver message discloses schema/indexes and enables enumeration.
15
+ const showMessage = !isProd || err.expose === true || (status >= 400 && status < 500);
11
16
  res.status(status).json({
12
- message: err.message || 'Server error',
13
- ...(process.env.NODE_ENV === 'production' ? {} : { stack: err.stack }),
17
+ message: showMessage ? (err.message || 'Server error') : 'Server error',
18
+ ...(isProd ? {} : { stack: err.stack }),
14
19
  });
15
20
  }
@@ -1 +0,0 @@
1
- scrypt$af7f6048a1c565afcc6f06de246f8b18$673c63bd180dc82eb74265fba34699ad45d1ae27353595e5c1c492351c66aafbd80e18d712b1e6f40e2343e45137b96cb4ed376d0c4ac5b54e8ea9d0a2fd2f73
@@ -1,139 +0,0 @@
1
- import React, { useState, useEffect, useCallback } from "react";
2
- import { motion, AnimatePresence } from "framer-motion";
3
- import { html, api, PAGE_TITLES } from "./lib.js";
4
- import { AppContext } from "./context.js";
5
- import { Sidebar } from "./components/Sidebar.js";
6
- import { ToastContainer } from "./components/ToastContainer.js";
7
- import { ConfirmDialog } from "./components/Modal.js";
8
- import { DashboardPage } from "./pages/DashboardPage.js";
9
- import { DatabasePage } from "./pages/DatabasePage.js";
10
- import { SchemasPage } from "./pages/SchemasPage.js";
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 { ApiTesterPage } from "./pages/ApiTesterPage.js";
16
- import { RealtimePage } from "./pages/RealtimePage.js";
17
- import { SerialPortPage } from "./pages/SerialPortPage.js";
18
- import { MailPage } from "./pages/MailPage.js";
19
- import { LogsPage } from "./pages/LogsPage.js";
20
- import { EnvironmentPage } from "./pages/EnvironmentPage.js";
21
-
22
- function getHashPage() {
23
- const hash = window.location.hash.replace("#/", "").replace("#", "");
24
- return hash && PAGE_TITLES[hash] ? hash : "dashboard";
25
- }
26
-
27
- export function App() {
28
- const [page, setPageState] = useState(getHashPage);
29
- const [data, setData] = useState({
30
- models: [],
31
- routes: [],
32
- mongoConfigured: false,
33
- routesDirExists: false,
34
- });
35
- const [status, setStatus] = useState({});
36
- const [toasts, setToasts] = useState([]);
37
- const [confirmState, setConfirmState] = useState(null);
38
-
39
- const showConfirm = useCallback((message, opts = {}) => {
40
- return new Promise((resolve) => {
41
- setConfirmState({ message, ...opts, resolve });
42
- });
43
- }, []);
44
-
45
- const setPage = useCallback((p) => {
46
- window.location.hash = "#/" + p;
47
- }, []);
48
-
49
- useEffect(() => {
50
- function onHash() {
51
- setPageState(getHashPage());
52
- }
53
- window.addEventListener("hashchange", onHash);
54
- return () => window.removeEventListener("hashchange", onHash);
55
- }, []);
56
-
57
- const toast = useCallback((message, type = "info") => {
58
- const id = Date.now();
59
- setToasts((prev) => [...prev, { id, message, type }]);
60
- setTimeout(
61
- () => setToasts((prev) => prev.filter((t) => t.id !== id)),
62
- 3500,
63
- );
64
- }, []);
65
-
66
- const refresh = useCallback(async () => {
67
- // File-writing actions (create route, docs, security, ...) modify index.js,
68
- // which triggers a nodemon restart. Since the dashboard is served by that same
69
- // app, the refresh fetch fired right after can hit the restart window and fail,
70
- // leaving the UI stale until a manual reload. Retry across the restart so new
71
- // state shows up in real time.
72
- let lastErr;
73
- for (let attempt = 0; attempt < 8; attempt++) {
74
- try {
75
- const [dashData, statusData] = await Promise.all([
76
- api("/dashboard"),
77
- api("/status"),
78
- ]);
79
- setData(dashData);
80
- setStatus(statusData);
81
- return;
82
- } catch (err) {
83
- lastErr = err;
84
- await new Promise((r) => setTimeout(r, 500));
85
- }
86
- }
87
- throw lastErr;
88
- }, []);
89
-
90
- useEffect(() => {
91
- refresh().catch(() => {});
92
- }, []);
93
-
94
- const pages = {
95
- dashboard: html`<${DashboardPage} />`,
96
- "ai-builder": html`<${AiBuilderPage} />`,
97
- database: html`<${DatabasePage} />`,
98
- schemas: html`<${SchemasPage} />`,
99
- routes: html`<${RoutesPage} />`,
100
- docs: html`<${DocsPage} />`,
101
- security: html`<${SecurityPage} />`,
102
- "api-tester": html`<${ApiTesterPage} />`,
103
- realtime: html`<${RealtimePage} />`,
104
- serialport: html`<${SerialPortPage} />`,
105
- mail: html`<${MailPage} />`,
106
- logs: html`<${LogsPage} />`,
107
- env: html`<${EnvironmentPage} />`,
108
- };
109
-
110
- return html`<${AppContext.Provider}
111
- value=${{ page, setPage, data, status, toasts, toast, refresh, showConfirm }}
112
- >
113
- <${Sidebar} />
114
- <div className="main">
115
- <div className="topbar">
116
- <h2>${PAGE_TITLES[page] || "Dashboard"}</h2>
117
- </div>
118
- <div className="content">
119
- <${AnimatePresence} mode="wait">
120
- <${motion.div} key=${page}
121
- initial=${{ opacity: 0, y: 8 }}
122
- animate=${{ opacity: 1, y: 0 }}
123
- exit=${{ opacity: 0, y: -8 }}
124
- transition=${{ duration: 0.15 }}>
125
- ${pages[page] || pages.dashboard}
126
- <//>
127
- <//>
128
- </div>
129
- </div>
130
- <${ToastContainer} />
131
- ${confirmState && html`<${ConfirmDialog}
132
- message=${confirmState.message}
133
- confirmLabel=${confirmState.confirmLabel}
134
- danger=${confirmState.danger}
135
- onConfirm=${() => { confirmState.resolve(true); setConfirmState(null); }}
136
- onCancel=${() => { confirmState.resolve(false); setConfirmState(null); }}
137
- />`}
138
- <//>`;
139
- }
@@ -1,40 +0,0 @@
1
- import React, { useState, useEffect } from "react";
2
- import { AnimatePresence } from "framer-motion";
3
- import { motion } from "framer-motion";
4
- import { html, API, getStoredPasskey } from "./lib.js";
5
- import { App } from "./App.js";
6
- import { PasskeyScreen } from "./screens/PasskeyScreen.js";
7
-
8
- export function Root() {
9
- const [authed, setAuthed] = useState(false);
10
- const [checking, setChecking] = useState(true);
11
-
12
- useEffect(() => {
13
- (async () => {
14
- try {
15
- const stored = getStoredPasskey();
16
- const res = await fetch(API + '/auth', {
17
- method: 'POST',
18
- headers: { 'Content-Type': 'application/json' },
19
- body: JSON.stringify({ passkey: stored }),
20
- });
21
- if (res.ok) setAuthed(true);
22
- } catch {}
23
- setChecking(false);
24
- })();
25
- }, []);
26
-
27
- if (checking) return null;
28
-
29
- return html`<${AnimatePresence} mode="wait">
30
- ${!authed
31
- ? html`<${PasskeyScreen} key="passkey" onSuccess=${() => setAuthed(true)} />`
32
- : html`<${motion.div} key="app"
33
- style=${{ display: 'flex', height: '100vh', width: '100%' }}
34
- initial=${{ opacity: 0 }}
35
- animate=${{ opacity: 1 }}
36
- transition=${{ duration: 0.4 }}>
37
- <${App} />
38
- <//>`}
39
- <//>`;
40
- }
@@ -1,52 +0,0 @@
1
- import React from "react";
2
- import { motion, AnimatePresence } from "framer-motion";
3
- import { LuTrash2, LuZap } from "react-icons/lu";
4
- import { html } from "../lib.js";
5
-
6
- export function Modal({ title, children, onClose, actions }) {
7
- return html`<${AnimatePresence}><${motion.div}
8
- className="modal-overlay"
9
- onClick=${(e) => e.target === e.currentTarget && onClose()}
10
- initial=${{ opacity: 0 }} animate=${{ opacity: 1 }} exit=${{ opacity: 0 }}
11
- transition=${{ duration: 0.15 }}
12
- >
13
- <${motion.div} className="modal"
14
- initial=${{ opacity: 0, scale: 0.95, y: 10 }}
15
- animate=${{ opacity: 1, scale: 1, y: 0 }}
16
- exit=${{ opacity: 0, scale: 0.95, y: 10 }}
17
- transition=${{ duration: 0.15 }}>
18
- <div className="modal-title">${title}</div>
19
- ${children}
20
- ${actions && html`<div className="modal-actions">${actions}</div>`}
21
- <//>
22
- <//><//>`;
23
- }
24
-
25
- export function ConfirmDialog({ message, onConfirm, onCancel, confirmLabel, danger }) {
26
- return html`<${AnimatePresence}><${motion.div}
27
- className="modal-overlay"
28
- onClick=${onCancel}
29
- initial=${{ opacity: 0 }} animate=${{ opacity: 1 }} exit=${{ opacity: 0 }}
30
- transition=${{ duration: 0.15 }}
31
- >
32
- <${motion.div} className="modal" onClick=${e => e.stopPropagation()}
33
- style=${{ minWidth: '380px', maxWidth: '440px' }}
34
- initial=${{ opacity: 0, scale: 0.95, y: 10 }}
35
- animate=${{ opacity: 1, scale: 1, y: 0 }}
36
- exit=${{ opacity: 0, scale: 0.95, y: 10 }}
37
- transition=${{ duration: 0.15 }}>
38
- <div className="flex items-center gap-3" style=${{ marginBottom: '16px' }}>
39
- <div style=${{ width: '40px', height: '40px', borderRadius: '10px', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
40
- background: danger ? 'var(--error-glow)' : 'var(--warning-glow)' }}>
41
- <${danger ? LuTrash2 : LuZap} size=${18} color=${danger ? 'var(--error)' : 'var(--warning)'} />
42
- </div>
43
- <div style=${{ fontSize: '15px', fontWeight: 600 }}>Confirm Action</div>
44
- </div>
45
- <div style=${{ fontSize: '13px', color: 'var(--text-secondary)', lineHeight: '1.6', marginBottom: '20px' }}>${message}</div>
46
- <div className="modal-actions">
47
- <button className="btn btn-ghost" onClick=${onCancel}>Cancel</button>
48
- <button className=${`btn ${danger ? 'btn-danger' : 'btn-primary'}`} onClick=${onConfirm}>${confirmLabel || 'Confirm'}</button>
49
- </div>
50
- <//>
51
- <//><//>`;
52
- }
@@ -1,73 +0,0 @@
1
- import React from "react";
2
- import { html, Icons } from "../lib.js";
3
- import { useApp } from "../context.js";
4
-
5
- export function Sidebar() {
6
- const { page, setPage, data } = useApp();
7
- const dbOk = data.mongoConfigured;
8
-
9
- const navItems = [
10
- {
11
- id: "dashboard",
12
- label: "Dashboard",
13
- icon: Icons.dashboard,
14
- section: "Overview",
15
- },
16
- // AI Builder — temporarily hidden from the menu (re-enable by uncommenting)
17
- // { id: "ai-builder", label: "AI Builder", icon: Icons.ai },
18
- {
19
- id: "database",
20
- label: "Database",
21
- icon: Icons.database,
22
- section: "Setup",
23
- },
24
- {
25
- id: "schemas",
26
- label: "Schemas",
27
- icon: Icons.schema,
28
- locked: !dbOk,
29
- lockMsg: "Requires database",
30
- },
31
- { id: "routes", label: "API Routes", icon: Icons.route },
32
- // { id: "docs", label: "API Docs", icon: Icons.docs }, // hidden for now
33
- { id: "api-tester", label: "API Tester", icon: Icons.apiTester },
34
- {
35
- id: "security",
36
- label: "Security",
37
- icon: Icons.shield,
38
- section: "Features",
39
- },
40
- { id: "realtime", label: "Real-time", icon: Icons.realtime },
41
- { id: "serialport", label: "SerialPort", icon: Icons.serial },
42
- // { id: "mail", label: "Email", icon: Icons.mail }, // hidden for now
43
- { id: "logs", label: "Logs", icon: Icons.logs, section: "Settings" },
44
- { id: "env", label: "Environment", icon: Icons.env },
45
- ];
46
-
47
- let currentSection = null;
48
- return html`<div className="sidebar">
49
- <div className="sidebar-brand">
50
- <pre>${`██╗ ██╗██████╗ ███╗ ██╗ ██████╗ ██████╗ ███████╗\n██║ ██║██╔══██╗████╗ ██║██╔═══██╗██╔══██╗██╔════╝\n███████║██████╔╝██╔██╗ ██║██║ ██║██║ ██║█████╗\n╚════██║██╔══██╗██║╚██╗██║██║ ██║██║ ██║██╔══╝\n ██║██████╔╝██║ ╚████║╚██████╔╝██████╔╝███████╗\n ╚═╝╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚══════╝`}</pre>
51
- </div>
52
- ${navItems.map((item) => {
53
- const sectionHeader =
54
- item.section && item.section !== currentSection
55
- ? ((currentSection = item.section),
56
- html`<div className="sidebar-section">${item.section}</div>`)
57
- : null;
58
- return html`<${React.Fragment} key=${item.id}>
59
- ${sectionHeader}
60
- <div
61
- className=${`sidebar-item ${page === item.id ? "active" : ""} ${item.locked ? "locked" : ""}`}
62
- onClick=${() => !item.locked && setPage(item.id)}
63
- title=${item.locked ? item.lockMsg : item.label}
64
- >
65
- <span className="sidebar-icon">${item.icon}</span>
66
- ${item.label}
67
- ${item.locked &&
68
- html`<span className="lock-icon">${Icons.lock}</span>`}
69
- </div>
70
- <//>`;
71
- })}
72
- </div>`;
73
- }
@@ -1,15 +0,0 @@
1
- import { html } from "../lib.js";
2
- import { useApp } from "../context.js";
3
-
4
- export function ToastContainer() {
5
- const { toasts } = useApp();
6
- if (toasts.length === 0) return null;
7
- return html`<div className="toast-container">
8
- ${toasts.map(
9
- (t) =>
10
- html`<div key=${t.id} className=${`toast ${t.type}`}>
11
- ${t.message}
12
- </div>`,
13
- )}
14
- </div>`;
15
- }
@@ -1,7 +0,0 @@
1
- import React, { createContext, useContext } from "react";
2
-
3
- export const AppContext = createContext();
4
-
5
- export function useApp() {
6
- return useContext(AppContext);
7
- }
@@ -1,113 +0,0 @@
1
- import React from "react";
2
- import htm from "htm";
3
- import {
4
- LuLayoutDashboard,
5
- LuDatabase,
6
- LuFileText,
7
- LuRadio,
8
- LuPlus,
9
- LuTrash2,
10
- LuCheck,
11
- LuLock,
12
- LuArrowRight,
13
- LuPencil,
14
- LuCable,
15
- LuActivity,
16
- LuZap,
17
- LuShield,
18
- LuBox,
19
- LuFlaskConical,
20
- LuSend,
21
- LuSettings,
22
- LuScrollText,
23
- LuSparkles,
24
- LuBookOpen,
25
- LuMail,
26
- } from "react-icons/lu";
27
-
28
- export const html = htm.bind(React.createElement);
29
- export const API = "/_dev/api";
30
-
31
- export function getStoredPasskey() {
32
- return sessionStorage.getItem("_dev_passkey") || "";
33
- }
34
-
35
- export function copyToClipboard(text) {
36
- if (navigator.clipboard && navigator.clipboard.writeText) {
37
- return navigator.clipboard.writeText(text);
38
- }
39
- const ta = document.createElement("textarea");
40
- ta.value = text;
41
- ta.style.cssText = "position:fixed;opacity:0";
42
- document.body.appendChild(ta);
43
- ta.select();
44
- document.execCommand("copy");
45
- document.body.removeChild(ta);
46
- return Promise.resolve();
47
- }
48
-
49
- export async function api(path, options = {}) {
50
- const passkey = getStoredPasskey();
51
- const headers = { "Content-Type": "application/json" };
52
- if (passkey) headers["x-passkey"] = passkey;
53
- const res = await fetch(API + path, {
54
- headers,
55
- ...options,
56
- body: options.body ? JSON.stringify(options.body) : undefined,
57
- });
58
- const text = await res.text();
59
- let data;
60
- try {
61
- data = JSON.parse(text);
62
- } catch {
63
- if (res.ok) return { message: "Done" };
64
- throw new Error("Server restarting, please wait...");
65
- }
66
- if (!res.ok) {
67
- const e = new Error(data.error || data.message || "Request failed");
68
- e.data = data; // expose extra fields (e.g. command output) to callers
69
- throw e;
70
- }
71
- return data;
72
- }
73
-
74
- export const Icons = {
75
- dashboard: html`<${LuLayoutDashboard} size=${16} />`,
76
- database: html`<${LuDatabase} size=${16} />`,
77
- schema: html`<${LuFileText} size=${16} />`,
78
- route: html`<${LuRadio} size=${16} />`,
79
- realtime: html`<${LuActivity} size=${16} />`,
80
- env: html`<${LuSettings} size=${16} />`,
81
- plus: html`<${LuPlus} size=${16} />`,
82
- trash: html`<${LuTrash2} size=${16} />`,
83
- check: html`<${LuCheck} size=${16} />`,
84
- lock: html`<${LuLock} size=${12} />`,
85
- arrow: html`<${LuArrowRight} size=${14} />`,
86
- edit: html`<${LuPencil} size=${16} />`,
87
- serial: html`<${LuCable} size=${16} />`,
88
- zap: html`<${LuZap} size=${16} />`,
89
- shield: html`<${LuShield} size=${16} />`,
90
- box: html`<${LuBox} size=${16} />`,
91
- apiTester: html`<${LuFlaskConical} size=${16} />`,
92
- send: html`<${LuSend} size=${16} />`,
93
- logs: html`<${LuScrollText} size=${16} />`,
94
- ai: html`<${LuSparkles} size=${16} />`,
95
- docs: html`<${LuBookOpen} size=${16} />`,
96
- mail: html`<${LuMail} size=${16} />`,
97
- };
98
-
99
- export const PAGE_TITLES = {
100
- dashboard: "Dashboard",
101
- "ai-builder": "AI Builder",
102
- database: "Database",
103
- schemas: "Schemas",
104
- routes: "API Routes",
105
- docs: "API Docs",
106
- security: "Security",
107
- "api-tester": "API Tester",
108
- realtime: "Real-time",
109
- serialport: "SerialPort",
110
- mail: "Email",
111
- logs: "Logs",
112
- env: "Environment",
113
- };
@@ -1,5 +0,0 @@
1
- import { createRoot } from "react-dom/client";
2
- import { html } from "./lib.js";
3
- import { Root } from "./Root.js";
4
-
5
- createRoot(document.getElementById("root")).render(html`<${Root} />`);