create-volt 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +6 -8
- package/package.json +1 -1
- package/templates/default/package.json +2 -2
- package/templates/default/public/app.js +0 -7
- package/templates/default/server.js +2 -7
- package/templates/default/setup/setup.js +0 -3
- package/templates/guestbook/package.json +5 -5
- package/templates/starter/package.json +2 -2
- package/templates/starter/public/app.js +0 -11
- package/templates/starter/server.js +2 -7
- package/templates/starter/setup/setup.js +0 -3
- package/addons/admin/files/lib/admin.js +0 -44
- package/addons/admin/files/public/admin-ui.js +0 -62
- package/addons/admin/meta.json +0 -8
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ All notable changes to `create-volt` are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/), and this project adheres to
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [0.14.0] - 2026-06-28
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- **Adopted the most-secure admin model: ephemeral, shell-only.** Removed the
|
|
11
|
+
persistent role-gated `admin` add-on (from 0.13.0). There is now **no web
|
|
12
|
+
admin** anywhere — the data browser is the ephemeral, localhost-only
|
|
13
|
+
`--studio`, and config is `--edit`; both are shell/SSH-gated. SECURITY.md
|
|
14
|
+
updated to state this as a core property.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- Dependency auto-update: `scripts/update-deps.mjs` + a weekly GitHub Action
|
|
18
|
+
bump create-volt's pinned dependency floors to the latest **within the current
|
|
19
|
+
major** (never a breaking major). Repo-only — scaffolded apps are untouched.
|
|
20
|
+
- Refreshed floors: express ^4.22.2, socket.io ^4.8.3, mongodb ^6.21.0,
|
|
21
|
+
mysql2 ^3.22.5, pg ^8.22.0, nodemailer ^6.10.1.
|
|
22
|
+
|
|
7
23
|
## [0.13.0] - 2026-06-28
|
|
8
24
|
|
|
9
25
|
### Added
|
|
@@ -187,6 +203,7 @@ All notable changes to `create-volt` are documented here. The format follows
|
|
|
187
203
|
watching and full-page hot reload. Supports `--skip-install` and `--force`,
|
|
188
204
|
and auto-detects npm / pnpm / yarn / bun for the install step.
|
|
189
205
|
|
|
206
|
+
[0.14.0]: https://github.com/MIR-2025/volt/releases/tag/v0.14.0
|
|
190
207
|
[0.13.0]: https://github.com/MIR-2025/volt/releases/tag/v0.13.0
|
|
191
208
|
[0.12.0]: https://github.com/MIR-2025/volt/releases/tag/v0.12.0
|
|
192
209
|
[0.11.0]: https://github.com/MIR-2025/volt/releases/tag/v0.11.0
|
package/README.md
CHANGED
|
@@ -82,14 +82,12 @@ MySQL / Postgres) to show data; the memory driver is per-process.
|
|
|
82
82
|
|
|
83
83
|
## Security & admin model
|
|
84
84
|
|
|
85
|
-
Volt is secure by default, and deliberately has **no
|
|
86
|
-
|
|
87
|
-
localhost-only processes; **shell/SSH access is the auth**.
|
|
88
|
-
each an SSH key — stronger than a shared web panel, with
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
browser-only admins. Plus: escaping by default (no XSS), server-side validation
|
|
92
|
-
+ caps, security headers, and `HttpOnly`+`SameSite` cookies. See
|
|
85
|
+
Volt is secure by default, and deliberately has **no web admin** — nothing like
|
|
86
|
+
`/wp-admin` anywhere. The config wizard (`--edit`) and data browser (`--studio`)
|
|
87
|
+
are ephemeral, localhost-only processes; **shell/SSH access is the auth**.
|
|
88
|
+
Several admins? Give each an SSH key — stronger than a shared web panel, with
|
|
89
|
+
nothing public to attack. Plus: escaping by default (no XSS), server-side
|
|
90
|
+
validation + caps, security headers, and `HttpOnly`+`SameSite` cookies. See
|
|
93
91
|
[SECURITY.md](https://github.com/MIR-2025/volt/blob/main/SECURITY.md).
|
|
94
92
|
|
|
95
93
|
## Updating Volt
|
package/package.json
CHANGED
|
@@ -86,11 +86,4 @@ if (enabled.includes("realtime")) {
|
|
|
86
86
|
/* realtime UI unavailable */
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
-
if (enabled.includes("admin")) {
|
|
90
|
-
try {
|
|
91
|
-
nodes.push((await import("/admin-ui.js")).adminPanel());
|
|
92
|
-
} catch {
|
|
93
|
-
/* admin UI unavailable */
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
89
|
mount("#app", ...nodes);
|
|
@@ -21,8 +21,8 @@ const ENV_PATH = path.join(__dirname, ".env");
|
|
|
21
21
|
const PKG_PATH = path.join(__dirname, "package.json");
|
|
22
22
|
const ADDONS_DIR = path.join(__dirname, ".volt", "addons"); // bundled add-on sources
|
|
23
23
|
const DEFAULT_PORT = 26628; // create-volt stamps this with the project's date-port
|
|
24
|
-
const PKG_VERSIONS = { mongodb: "^6.
|
|
25
|
-
const LIB_FILE = { db: "store.js", mailer: "mailer.js", auth: "auth.js", realtime: "realtime.js"
|
|
24
|
+
const PKG_VERSIONS = { mongodb: "^6.21.0", mysql2: "^3.22.5", pg: "^8.22.0", nodemailer: "^6.10.1" };
|
|
25
|
+
const LIB_FILE = { db: "store.js", mailer: "mailer.js", auth: "auth.js", realtime: "realtime.js" };
|
|
26
26
|
|
|
27
27
|
// --- tiny .env loader (no dependency); never overrides an existing env var ---
|
|
28
28
|
function readEnvFile() {
|
|
@@ -101,11 +101,6 @@ async function startApp() {
|
|
|
101
101
|
if (enabled.has("db")) store = await (await addonMod("db")).createStore();
|
|
102
102
|
if (enabled.has("mailer")) mailer = await (await addonMod("mailer")).createMailer();
|
|
103
103
|
if (enabled.has("auth") && store && mailer) app.use((await addonMod("auth")).authRouter({ store, mailer }));
|
|
104
|
-
if (enabled.has("admin") && store && enabled.has("auth")) {
|
|
105
|
-
const adminEmails = String(process.env.ADMIN_EMAILS || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
106
|
-
const requireAuth = (await addonMod("auth")).requireAuth(store);
|
|
107
|
-
app.use((await addonMod("admin")).adminRouter({ store, requireAuth, adminEmails }));
|
|
108
|
-
}
|
|
109
104
|
|
|
110
105
|
// expose which add-ons are on, and serve each enabled add-on's frontend assets
|
|
111
106
|
app.get("/__volt/addons", (_req, res) => res.json([...enabled]));
|
|
@@ -17,7 +17,6 @@ const state = signal({
|
|
|
17
17
|
dbUrl: current.DATABASE_URL || "",
|
|
18
18
|
smtpUrl: current.SMTP_URL || "",
|
|
19
19
|
mailFrom: current.MAIL_FROM || "",
|
|
20
|
-
adminEmails: current.ADMIN_EMAILS || "",
|
|
21
20
|
port: current.PORT || String(defaultPort),
|
|
22
21
|
});
|
|
23
22
|
const set = (patch) => state({ ...state(), ...patch });
|
|
@@ -54,7 +53,6 @@ function genEnv(s) {
|
|
|
54
53
|
else out.push("# SMTP_URL= # unset → emails print to the console");
|
|
55
54
|
if (s.mailFrom) out.push(`MAIL_FROM=${clean(s.mailFrom)}`);
|
|
56
55
|
}
|
|
57
|
-
if (eff.includes("admin")) out.push(`ADMIN_EMAILS=${clean(s.adminEmails)}`);
|
|
58
56
|
return out.join("\n") + "\n";
|
|
59
57
|
}
|
|
60
58
|
const env = computed(() => genEnv(state()));
|
|
@@ -150,7 +148,6 @@ mount(
|
|
|
150
148
|
${field("PORT", "port", String(defaultPort))}
|
|
151
149
|
${() => (eff().includes("db") ? dbSettings() : null)}
|
|
152
150
|
${() => (eff().includes("mailer") ? html`${field("SMTP_URL (optional)", "smtpUrl", "smtp://user:pass@smtp.host:587")}${field("MAIL_FROM", "mailFrom", "App <no-reply@you.com>")}` : null)}
|
|
153
|
-
${() => (eff().includes("admin") ? field("ADMIN_EMAILS (comma-separated)", "adminEmails", "you@example.com") : null)}
|
|
154
151
|
</div>`,
|
|
155
152
|
html`<div class="card-x p-4 mb-3">
|
|
156
153
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
"dev": "node server.js"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"express": "^4.
|
|
14
|
-
"socket.io": "^4.
|
|
13
|
+
"express": "^4.22.2",
|
|
14
|
+
"socket.io": "^4.8.3"
|
|
15
15
|
},
|
|
16
16
|
"optionalDependencies": {
|
|
17
|
-
"mongodb": "^6.
|
|
18
|
-
"mysql2": "^3.
|
|
19
|
-
"pg": "^8.
|
|
17
|
+
"mongodb": "^6.21.0",
|
|
18
|
+
"mysql2": "^3.22.5",
|
|
19
|
+
"pg": "^8.22.0"
|
|
20
20
|
}
|
|
21
21
|
}
|
|
@@ -137,21 +137,10 @@ if (hasChat) {
|
|
|
137
137
|
/* chat UI unavailable */
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
|
-
// Admin tab appears only for users in the ADMIN_EMAILS allowlist.
|
|
141
|
-
if (enabled.includes("admin")) {
|
|
142
|
-
try {
|
|
143
|
-
const r = await (await fetch("/admin/api/me")).json();
|
|
144
|
-
if (r.isAdmin) sections.admin = (await import("/admin-ui.js")).adminPanel();
|
|
145
|
-
} catch {
|
|
146
|
-
/* not signed in / not admin */
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
140
|
const TABS = [
|
|
151
141
|
["home", "Home"],
|
|
152
142
|
...(hasAuth ? [["notes", "Notes"]] : []),
|
|
153
143
|
...(sections.chat ? [["chat", "Chat"]] : []),
|
|
154
|
-
...(sections.admin ? [["admin", "Admin"]] : []),
|
|
155
144
|
["account", "Account"],
|
|
156
145
|
];
|
|
157
146
|
|
|
@@ -22,8 +22,8 @@ const ENV_PATH = path.join(__dirname, ".env");
|
|
|
22
22
|
const PKG_PATH = path.join(__dirname, "package.json");
|
|
23
23
|
const ADDONS_DIR = path.join(__dirname, ".volt", "addons"); // bundled add-on sources
|
|
24
24
|
const DEFAULT_PORT = 26628; // create-volt stamps this with the project's date-port
|
|
25
|
-
const PKG_VERSIONS = { mongodb: "^6.
|
|
26
|
-
const LIB_FILE = { db: "store.js", mailer: "mailer.js", auth: "auth.js", realtime: "realtime.js"
|
|
25
|
+
const PKG_VERSIONS = { mongodb: "^6.21.0", mysql2: "^3.22.5", pg: "^8.22.0", nodemailer: "^6.10.1" };
|
|
26
|
+
const LIB_FILE = { db: "store.js", mailer: "mailer.js", auth: "auth.js", realtime: "realtime.js" };
|
|
27
27
|
|
|
28
28
|
// --- tiny .env loader (no dependency); never overrides an existing env var ---
|
|
29
29
|
function readEnvFile() {
|
|
@@ -102,11 +102,6 @@ async function startApp() {
|
|
|
102
102
|
if (enabled.has("db")) store = await (await addonMod("db")).createStore();
|
|
103
103
|
if (enabled.has("mailer")) mailer = await (await addonMod("mailer")).createMailer();
|
|
104
104
|
if (enabled.has("auth") && store && mailer) app.use((await addonMod("auth")).authRouter({ store, mailer }));
|
|
105
|
-
if (enabled.has("admin") && store && enabled.has("auth")) {
|
|
106
|
-
const adminEmails = String(process.env.ADMIN_EMAILS || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
107
|
-
const requireAuth = (await addonMod("auth")).requireAuth(store);
|
|
108
|
-
app.use((await addonMod("admin")).adminRouter({ store, requireAuth, adminEmails }));
|
|
109
|
-
}
|
|
110
105
|
|
|
111
106
|
// notes — a per-user CRUD example (auth-gated, owner-scoped, db-backed)
|
|
112
107
|
if (enabled.has("db") && enabled.has("auth") && store) {
|
|
@@ -17,7 +17,6 @@ const state = signal({
|
|
|
17
17
|
dbUrl: current.DATABASE_URL || "",
|
|
18
18
|
smtpUrl: current.SMTP_URL || "",
|
|
19
19
|
mailFrom: current.MAIL_FROM || "",
|
|
20
|
-
adminEmails: current.ADMIN_EMAILS || "",
|
|
21
20
|
port: current.PORT || String(defaultPort),
|
|
22
21
|
});
|
|
23
22
|
const set = (patch) => state({ ...state(), ...patch });
|
|
@@ -54,7 +53,6 @@ function genEnv(s) {
|
|
|
54
53
|
else out.push("# SMTP_URL= # unset → emails print to the console");
|
|
55
54
|
if (s.mailFrom) out.push(`MAIL_FROM=${clean(s.mailFrom)}`);
|
|
56
55
|
}
|
|
57
|
-
if (eff.includes("admin")) out.push(`ADMIN_EMAILS=${clean(s.adminEmails)}`);
|
|
58
56
|
return out.join("\n") + "\n";
|
|
59
57
|
}
|
|
60
58
|
const env = computed(() => genEnv(state()));
|
|
@@ -150,7 +148,6 @@ mount(
|
|
|
150
148
|
${field("PORT", "port", String(defaultPort))}
|
|
151
149
|
${() => (eff().includes("db") ? dbSettings() : null)}
|
|
152
150
|
${() => (eff().includes("mailer") ? html`${field("SMTP_URL (optional)", "smtpUrl", "smtp://user:pass@smtp.host:587")}${field("MAIL_FROM", "mailFrom", "App <no-reply@you.com>")}` : null)}
|
|
153
|
-
${() => (eff().includes("admin") ? field("ADMIN_EMAILS (comma-separated)", "adminEmails", "you@example.com") : null)}
|
|
154
151
|
</div>`,
|
|
155
152
|
html`<div class="card-x p-4 mb-3">
|
|
156
153
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
// admin.js — opt-in, role-gated web admin (data browser) for the running app.
|
|
2
|
-
// This is the ONE deliberately-persistent privileged surface in Volt, for
|
|
3
|
-
// browser-only admins. Every route requires a session AND membership in the
|
|
4
|
-
// ADMIN_EMAILS allowlist. Internal collections (auth tokens/sessions) are hidden.
|
|
5
|
-
// Prefer `npm run dev -- --studio` (ephemeral) unless you truly need this.
|
|
6
|
-
|
|
7
|
-
import express from "express";
|
|
8
|
-
|
|
9
|
-
const HIDDEN = new Set(["auth_tokens", "auth_sessions", "__voltcheck"]);
|
|
10
|
-
const visible = (n) => n && !HIDDEN.has(n);
|
|
11
|
-
|
|
12
|
-
export function adminRouter({ store, requireAuth, adminEmails }) {
|
|
13
|
-
const allow = new Set((adminEmails || []).map((e) => String(e).trim().toLowerCase()).filter(Boolean));
|
|
14
|
-
const isAdmin = (email) => allow.has(String(email || "").toLowerCase());
|
|
15
|
-
|
|
16
|
-
const r = express.Router();
|
|
17
|
-
r.use(requireAuth); // must be signed in for anything under /admin/api
|
|
18
|
-
|
|
19
|
-
// any signed-in user may ask whether *they* are an admin (drives the UI)
|
|
20
|
-
r.get("/admin/api/me", (req, res) => res.json({ email: req.user.email, isAdmin: isAdmin(req.user.email) }));
|
|
21
|
-
|
|
22
|
-
// everything below is admins-only
|
|
23
|
-
r.use((req, res, next) => (isAdmin(req.user.email) ? next() : res.status(403).json({ ok: false, error: "Admins only." })));
|
|
24
|
-
|
|
25
|
-
r.get("/admin/api/collections", async (_req, res) => {
|
|
26
|
-
const all = (await store.collections()) || [];
|
|
27
|
-
res.json({ driver: store.name, collections: all.filter(visible) });
|
|
28
|
-
});
|
|
29
|
-
r.get("/admin/api/collection", async (req, res) => {
|
|
30
|
-
const name = String(req.query.name || "");
|
|
31
|
-
if (!visible(name)) return res.status(403).json({ ok: false, error: "hidden" });
|
|
32
|
-
res.json({ ok: true, name, docs: (await store.collection(name).all()).slice(0, 500) });
|
|
33
|
-
});
|
|
34
|
-
r.delete("/admin/api/doc", async (req, res) => {
|
|
35
|
-
const name = String(req.query.name || "");
|
|
36
|
-
const id = String(req.query.id || "");
|
|
37
|
-
if (!visible(name)) return res.status(403).json({ ok: false, error: "hidden" });
|
|
38
|
-
if (!id) return res.status(400).json({ ok: false, error: "missing id" });
|
|
39
|
-
await store.collection(name).delete(id);
|
|
40
|
-
res.json({ ok: true });
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
return r;
|
|
44
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
// admin-ui.js — the role-gated admin panel (frontend for the admin add-on).
|
|
2
|
-
// Renders nothing for non-admins; for admins, a data browser over /admin/api/*.
|
|
3
|
-
// Mounted by the app when the admin add-on is enabled.
|
|
4
|
-
import { signal, html } from "/volt.js";
|
|
5
|
-
|
|
6
|
-
const j = async (url, opts) => {
|
|
7
|
-
const res = await fetch(url, opts);
|
|
8
|
-
return { status: res.status, body: await res.json().catch(() => ({})) };
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export function adminPanel() {
|
|
12
|
-
const ready = signal(false);
|
|
13
|
-
const isAdmin = signal(false);
|
|
14
|
-
const driver = signal("");
|
|
15
|
-
const collections = signal([]);
|
|
16
|
-
const current = signal("");
|
|
17
|
-
const docs = signal([]);
|
|
18
|
-
const note = signal("");
|
|
19
|
-
|
|
20
|
-
async function init() {
|
|
21
|
-
const { status, body } = await j("/admin/api/me");
|
|
22
|
-
if (status === 200) isAdmin(!!body.isAdmin);
|
|
23
|
-
ready(true);
|
|
24
|
-
if (isAdmin()) load();
|
|
25
|
-
}
|
|
26
|
-
async function load() {
|
|
27
|
-
const { body } = await j("/admin/api/collections");
|
|
28
|
-
driver(body.driver || "");
|
|
29
|
-
collections(body.collections || []);
|
|
30
|
-
if (!current() && collections().length) open(collections()[0]);
|
|
31
|
-
}
|
|
32
|
-
async function open(name) {
|
|
33
|
-
current(name);
|
|
34
|
-
const { body } = await j(`/admin/api/collection?name=${encodeURIComponent(name)}`);
|
|
35
|
-
docs(body.ok ? body.docs : []);
|
|
36
|
-
}
|
|
37
|
-
async function del(id) {
|
|
38
|
-
await j(`/admin/api/doc?name=${encodeURIComponent(current())}&id=${encodeURIComponent(id)}`, { method: "DELETE" });
|
|
39
|
-
docs(docs().filter((d) => d.id !== id));
|
|
40
|
-
note("Deleted.");
|
|
41
|
-
}
|
|
42
|
-
init();
|
|
43
|
-
|
|
44
|
-
const tab = (name) =>
|
|
45
|
-
html`<button class=${() => "btn btn-sm " + (current() === name ? "btn-primary" : "btn-outline-secondary")} onclick=${() => open(name)}>${name}</button>`;
|
|
46
|
-
const row = (d) =>
|
|
47
|
-
html`<div class="d-flex justify-content-between align-items-start gap-2 py-1" style="border-top:1px solid #232a36">
|
|
48
|
-
<pre class="mb-0 small flex-grow-1" style="white-space:pre-wrap;color:#cfe3ff">${JSON.stringify(d, null, 2)}</pre>
|
|
49
|
-
<button class="btn btn-sm btn-outline-danger" onclick=${() => del(d.id)}>✕</button>
|
|
50
|
-
</div>`;
|
|
51
|
-
|
|
52
|
-
// hidden entirely for non-admins (and until we know)
|
|
53
|
-
return html`${() =>
|
|
54
|
-
!ready() || !isAdmin()
|
|
55
|
-
? null
|
|
56
|
-
: html`<div class="card-x p-4 mb-4">
|
|
57
|
-
<h2 class="h6 mb-3">Admin <span class="text-muted small">— data ${() => (driver() ? "· " + driver() : "")}</span></h2>
|
|
58
|
-
<div class="d-flex flex-wrap gap-1 mb-2">${() => (collections().length ? collections().map(tab) : html`<span class="text-muted small">No collections yet.</span>`)}</div>
|
|
59
|
-
<div style="max-height:260px;overflow:auto">${() => (docs().length ? docs().map(row) : html`<span class="text-muted small">Empty.</span>`)}</div>
|
|
60
|
-
${() => (note() ? html`<p class="small text-muted mb-0 mt-2">${note}</p>` : null)}
|
|
61
|
-
</div>`}`;
|
|
62
|
-
}
|
package/addons/admin/meta.json
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"description": "Opt-in role-gated web admin: a persistent /admin data browser for browser-only admins (auth + ADMIN_EMAILS allowlist). Adds a standing surface — prefer `--studio` unless you need non-shell admins.",
|
|
3
|
-
"dependsOn": ["db", "auth"],
|
|
4
|
-
"sentinel": "lib/admin.js",
|
|
5
|
-
"install": ["express"],
|
|
6
|
-
"optional": {},
|
|
7
|
-
"wiring": "Set ADMIN_EMAILS in .env to a comma-separated allowlist of admin emails. Only those signed-in users see the admin panel and can reach /admin/api/*."
|
|
8
|
-
}
|