create-volt 0.50.0 → 0.52.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 +20 -0
- package/addons/mailer/files/lib/mailer.js +19 -8
- package/package.json +1 -1
- package/templates/blog/server.js +11 -0
- package/templates/blog/setup/setup.js +16 -0
- package/templates/default/server.js +11 -0
- package/templates/default/setup/setup.js +16 -0
- package/templates/docs/server.js +11 -0
- package/templates/docs/setup/setup.js +16 -0
- package/templates/starter/server.js +11 -0
- package/templates/starter/setup/setup.js +16 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@ 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.52.0] - 2026-07-04
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Mailer accepts discrete SMTP vars.** The mailer add-on builds its transport
|
|
11
|
+
from `SMTP_HOST`/`SMTP_PORT`/`SMTP_SECURE`/`SMTP_USER`/`SMTP_PASS` when
|
|
12
|
+
`SMTP_URL` is not set (secure defaults on for port 465), and resolves the From
|
|
13
|
+
address as `MAIL_FROM` → `SMTP_FROM` → `SMTP_USER`. So a plain host/port/user/pass
|
|
14
|
+
config works without composing a URL.
|
|
15
|
+
|
|
16
|
+
## [0.51.0] - 2026-06-30
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **Generate a hosted-AI token from the config.** The wizard's AI section has a
|
|
20
|
+
**Generate a free hosted token** button: it self-registers with the voltjs.com
|
|
21
|
+
gateway (new public, rate-limited `POST /api/register`) and writes
|
|
22
|
+
`VOLT_AI_TOKEN` to `.env` on Apply. One click to the free-capped/pay-as-you-go
|
|
23
|
+
hosted tier when you have no key of your own.
|
|
24
|
+
|
|
7
25
|
## [0.50.0] - 2026-06-30
|
|
8
26
|
|
|
9
27
|
### Added
|
|
@@ -666,6 +684,8 @@ All notable changes to `create-volt` are documented here. The format follows
|
|
|
666
684
|
watching and full-page hot reload. Supports `--skip-install` and `--force`,
|
|
667
685
|
and auto-detects npm / pnpm / yarn / bun for the install step.
|
|
668
686
|
|
|
687
|
+
[0.52.0]: https://github.com/MIR-2025/volt/releases/tag/v0.52.0
|
|
688
|
+
[0.51.0]: https://github.com/MIR-2025/volt/releases/tag/v0.51.0
|
|
669
689
|
[0.50.0]: https://github.com/MIR-2025/volt/releases/tag/v0.50.0
|
|
670
690
|
[0.49.0]: https://github.com/MIR-2025/volt/releases/tag/v0.49.0
|
|
671
691
|
[0.48.3]: https://github.com/MIR-2025/volt/releases/tag/v0.48.3
|
|
@@ -1,20 +1,31 @@
|
|
|
1
|
-
// mailer.js — sends email. In dev (no
|
|
2
|
-
// console so you can see them; in production it uses nodemailer when
|
|
3
|
-
//
|
|
1
|
+
// mailer.js — sends email. In dev (no SMTP config) it prints messages to the
|
|
2
|
+
// console so you can see them; in production it uses nodemailer when SMTP is
|
|
3
|
+
// configured — either a single SMTP_URL, or discrete SMTP_HOST/SMTP_PORT/
|
|
4
|
+
// SMTP_SECURE/SMTP_USER/SMTP_PASS vars — and the package is installed.
|
|
4
5
|
|
|
5
6
|
export async function createMailer() {
|
|
6
|
-
|
|
7
|
-
const
|
|
7
|
+
// transport: SMTP_URL wins; otherwise build it from discrete host/port vars.
|
|
8
|
+
const transportConfig = process.env.SMTP_URL
|
|
9
|
+
? process.env.SMTP_URL
|
|
10
|
+
: process.env.SMTP_HOST
|
|
11
|
+
? {
|
|
12
|
+
host: process.env.SMTP_HOST,
|
|
13
|
+
port: Number(process.env.SMTP_PORT) || 587,
|
|
14
|
+
secure: /^(1|true|yes|on)$/i.test(process.env.SMTP_SECURE || "") || Number(process.env.SMTP_PORT) === 465,
|
|
15
|
+
auth: process.env.SMTP_USER ? { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS } : undefined,
|
|
16
|
+
}
|
|
17
|
+
: null;
|
|
18
|
+
const from = process.env.MAIL_FROM || process.env.SMTP_FROM || process.env.SMTP_USER || "App <no-reply@example.com>";
|
|
8
19
|
|
|
9
|
-
if (
|
|
20
|
+
if (transportConfig) {
|
|
10
21
|
let nodemailer;
|
|
11
22
|
try {
|
|
12
23
|
nodemailer = (await import("nodemailer")).default;
|
|
13
24
|
} catch {
|
|
14
|
-
console.warn("[mailer]
|
|
25
|
+
console.warn("[mailer] SMTP configured but 'nodemailer' isn't installed — using console. Run: npm install nodemailer");
|
|
15
26
|
}
|
|
16
27
|
if (nodemailer) {
|
|
17
|
-
const transport = nodemailer.createTransport(
|
|
28
|
+
const transport = nodemailer.createTransport(transportConfig);
|
|
18
29
|
return {
|
|
19
30
|
name: "smtp",
|
|
20
31
|
async send({ to, subject, text, html }) {
|
package/package.json
CHANGED
package/templates/blog/server.js
CHANGED
|
@@ -344,6 +344,17 @@ function startSetup() {
|
|
|
344
344
|
}
|
|
345
345
|
return;
|
|
346
346
|
}
|
|
347
|
+
// --- generate a free hosted-AI token from the gateway (self-service) ---
|
|
348
|
+
if (req.method === "POST" && p === "/setup/gen-token") {
|
|
349
|
+
res.setHeader("Content-Type", "application/json");
|
|
350
|
+
const env = readEnvFile();
|
|
351
|
+
const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
|
|
352
|
+
fetch(base + "/api/register", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ app: env.SITE_NAME || "volt-app" }) })
|
|
353
|
+
.then((r) => r.json())
|
|
354
|
+
.then((j) => res.end(JSON.stringify(j)))
|
|
355
|
+
.catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
347
358
|
// --- AI proxy for the in-config editor (RTEPro). Uses a local provider key
|
|
348
359
|
// (BYO) when set; otherwise falls back to the voltjs.com gateway via
|
|
349
360
|
// VOLT_AI_TOKEN (free-capped, then pay-as-you-go on the host's key). The
|
|
@@ -34,6 +34,7 @@ const state = signal({
|
|
|
34
34
|
theme: current.THEME || "",
|
|
35
35
|
aiProvider: current.AI_PROVIDER || "anthropic",
|
|
36
36
|
aiKey: current.ANTHROPIC_API_KEY || current.OPENAI_API_KEY || current.GEMINI_API_KEY || "",
|
|
37
|
+
aiToken: current.VOLT_AI_TOKEN || "",
|
|
37
38
|
});
|
|
38
39
|
const set = (patch) => state({ ...state(), ...patch });
|
|
39
40
|
const toggle = (n) => state({ ...state(), addons: { ...state().addons, [n]: !state().addons[n] } });
|
|
@@ -82,6 +83,7 @@ function genEnv(s) {
|
|
|
82
83
|
const keyVar = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", gemini: "GEMINI_API_KEY" }[s.aiProvider] || "ANTHROPIC_API_KEY";
|
|
83
84
|
out.push(`${keyVar}=${clean(s.aiKey)}`);
|
|
84
85
|
}
|
|
86
|
+
if (s.aiToken) out.push(`VOLT_AI_TOKEN=${clean(s.aiToken)}`); // hosted-tier token (used when no local key)
|
|
85
87
|
if (eff.includes("db")) {
|
|
86
88
|
out.push(`DB_DRIVER=${clean(s.dbDriver)}`);
|
|
87
89
|
if (s.dbDriver === "mongodb") {
|
|
@@ -266,6 +268,8 @@ const aiSettings = () =>
|
|
|
266
268
|
</select>
|
|
267
269
|
${() => html`<a class="small d-inline-block mb-1" href=${AI_KEY_URL[state().aiProvider] || AI_KEY_URL.anthropic} target="_blank" rel="noopener">Get a ${state().aiProvider} key → paste it below (stays server-side in .env)</a>`}
|
|
268
270
|
${field("API key", "aiKey", "sk-…")}
|
|
271
|
+
<div class="small text-muted mt-2 mb-1">— or — no key? Use the hosted tier (free, capped, then pay-as-you-go):</div>
|
|
272
|
+
${() => (state().aiToken ? html`<div class="small">Hosted token: <code>${state().aiToken.slice(0, 14)}…</code> <button class="btn btn-sm btn-link p-0 ms-1" onclick=${() => set({ aiToken: "" })}>clear</button></div>` : html`<button class="btn btn-sm btn-outline-secondary" onclick=${genToken}>Generate a free hosted token</button>`)}
|
|
269
273
|
</div>
|
|
270
274
|
</details>`;
|
|
271
275
|
|
|
@@ -304,6 +308,18 @@ async function buyCredits(amountUsd) {
|
|
|
304
308
|
status("Checkout request failed.");
|
|
305
309
|
}
|
|
306
310
|
}
|
|
311
|
+
async function genToken() {
|
|
312
|
+
status("Requesting a hosted token…");
|
|
313
|
+
try {
|
|
314
|
+
const r = await (await fetch("/setup/gen-token", { method: "POST" })).json();
|
|
315
|
+
if (r.ok && r.token) {
|
|
316
|
+
set({ aiToken: r.token });
|
|
317
|
+
status("Hosted token generated — click Apply to save it.");
|
|
318
|
+
} else status("Could not get a token: " + (r.error || "?"));
|
|
319
|
+
} catch {
|
|
320
|
+
status("Token request failed.");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
307
323
|
const items = signal({ pages: [], posts: [] });
|
|
308
324
|
const editing = signal(null); // { type, slug, title, isNew } — set only on open/save/close
|
|
309
325
|
let ed = null; // live RTEPro instance for the open editor
|
|
@@ -344,6 +344,17 @@ function startSetup() {
|
|
|
344
344
|
}
|
|
345
345
|
return;
|
|
346
346
|
}
|
|
347
|
+
// --- generate a free hosted-AI token from the gateway (self-service) ---
|
|
348
|
+
if (req.method === "POST" && p === "/setup/gen-token") {
|
|
349
|
+
res.setHeader("Content-Type", "application/json");
|
|
350
|
+
const env = readEnvFile();
|
|
351
|
+
const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
|
|
352
|
+
fetch(base + "/api/register", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ app: env.SITE_NAME || "volt-app" }) })
|
|
353
|
+
.then((r) => r.json())
|
|
354
|
+
.then((j) => res.end(JSON.stringify(j)))
|
|
355
|
+
.catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
347
358
|
// --- AI proxy for the in-config editor (RTEPro). Uses a local provider key
|
|
348
359
|
// (BYO) when set; otherwise falls back to the voltjs.com gateway via
|
|
349
360
|
// VOLT_AI_TOKEN (free-capped, then pay-as-you-go on the host's key). The
|
|
@@ -34,6 +34,7 @@ const state = signal({
|
|
|
34
34
|
theme: current.THEME || "",
|
|
35
35
|
aiProvider: current.AI_PROVIDER || "anthropic",
|
|
36
36
|
aiKey: current.ANTHROPIC_API_KEY || current.OPENAI_API_KEY || current.GEMINI_API_KEY || "",
|
|
37
|
+
aiToken: current.VOLT_AI_TOKEN || "",
|
|
37
38
|
});
|
|
38
39
|
const set = (patch) => state({ ...state(), ...patch });
|
|
39
40
|
const toggle = (n) => state({ ...state(), addons: { ...state().addons, [n]: !state().addons[n] } });
|
|
@@ -82,6 +83,7 @@ function genEnv(s) {
|
|
|
82
83
|
const keyVar = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", gemini: "GEMINI_API_KEY" }[s.aiProvider] || "ANTHROPIC_API_KEY";
|
|
83
84
|
out.push(`${keyVar}=${clean(s.aiKey)}`);
|
|
84
85
|
}
|
|
86
|
+
if (s.aiToken) out.push(`VOLT_AI_TOKEN=${clean(s.aiToken)}`); // hosted-tier token (used when no local key)
|
|
85
87
|
if (eff.includes("db")) {
|
|
86
88
|
out.push(`DB_DRIVER=${clean(s.dbDriver)}`);
|
|
87
89
|
if (s.dbDriver === "mongodb") {
|
|
@@ -266,6 +268,8 @@ const aiSettings = () =>
|
|
|
266
268
|
</select>
|
|
267
269
|
${() => html`<a class="small d-inline-block mb-1" href=${AI_KEY_URL[state().aiProvider] || AI_KEY_URL.anthropic} target="_blank" rel="noopener">Get a ${state().aiProvider} key → paste it below (stays server-side in .env)</a>`}
|
|
268
270
|
${field("API key", "aiKey", "sk-…")}
|
|
271
|
+
<div class="small text-muted mt-2 mb-1">— or — no key? Use the hosted tier (free, capped, then pay-as-you-go):</div>
|
|
272
|
+
${() => (state().aiToken ? html`<div class="small">Hosted token: <code>${state().aiToken.slice(0, 14)}…</code> <button class="btn btn-sm btn-link p-0 ms-1" onclick=${() => set({ aiToken: "" })}>clear</button></div>` : html`<button class="btn btn-sm btn-outline-secondary" onclick=${genToken}>Generate a free hosted token</button>`)}
|
|
269
273
|
</div>
|
|
270
274
|
</details>`;
|
|
271
275
|
|
|
@@ -304,6 +308,18 @@ async function buyCredits(amountUsd) {
|
|
|
304
308
|
status("Checkout request failed.");
|
|
305
309
|
}
|
|
306
310
|
}
|
|
311
|
+
async function genToken() {
|
|
312
|
+
status("Requesting a hosted token…");
|
|
313
|
+
try {
|
|
314
|
+
const r = await (await fetch("/setup/gen-token", { method: "POST" })).json();
|
|
315
|
+
if (r.ok && r.token) {
|
|
316
|
+
set({ aiToken: r.token });
|
|
317
|
+
status("Hosted token generated — click Apply to save it.");
|
|
318
|
+
} else status("Could not get a token: " + (r.error || "?"));
|
|
319
|
+
} catch {
|
|
320
|
+
status("Token request failed.");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
307
323
|
const items = signal({ pages: [], posts: [] });
|
|
308
324
|
const editing = signal(null); // { type, slug, title, isNew } — set only on open/save/close
|
|
309
325
|
let ed = null; // live RTEPro instance for the open editor
|
package/templates/docs/server.js
CHANGED
|
@@ -344,6 +344,17 @@ function startSetup() {
|
|
|
344
344
|
}
|
|
345
345
|
return;
|
|
346
346
|
}
|
|
347
|
+
// --- generate a free hosted-AI token from the gateway (self-service) ---
|
|
348
|
+
if (req.method === "POST" && p === "/setup/gen-token") {
|
|
349
|
+
res.setHeader("Content-Type", "application/json");
|
|
350
|
+
const env = readEnvFile();
|
|
351
|
+
const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
|
|
352
|
+
fetch(base + "/api/register", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ app: env.SITE_NAME || "volt-app" }) })
|
|
353
|
+
.then((r) => r.json())
|
|
354
|
+
.then((j) => res.end(JSON.stringify(j)))
|
|
355
|
+
.catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
347
358
|
// --- AI proxy for the in-config editor (RTEPro). Uses a local provider key
|
|
348
359
|
// (BYO) when set; otherwise falls back to the voltjs.com gateway via
|
|
349
360
|
// VOLT_AI_TOKEN (free-capped, then pay-as-you-go on the host's key). The
|
|
@@ -34,6 +34,7 @@ const state = signal({
|
|
|
34
34
|
theme: current.THEME || "",
|
|
35
35
|
aiProvider: current.AI_PROVIDER || "anthropic",
|
|
36
36
|
aiKey: current.ANTHROPIC_API_KEY || current.OPENAI_API_KEY || current.GEMINI_API_KEY || "",
|
|
37
|
+
aiToken: current.VOLT_AI_TOKEN || "",
|
|
37
38
|
});
|
|
38
39
|
const set = (patch) => state({ ...state(), ...patch });
|
|
39
40
|
const toggle = (n) => state({ ...state(), addons: { ...state().addons, [n]: !state().addons[n] } });
|
|
@@ -82,6 +83,7 @@ function genEnv(s) {
|
|
|
82
83
|
const keyVar = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", gemini: "GEMINI_API_KEY" }[s.aiProvider] || "ANTHROPIC_API_KEY";
|
|
83
84
|
out.push(`${keyVar}=${clean(s.aiKey)}`);
|
|
84
85
|
}
|
|
86
|
+
if (s.aiToken) out.push(`VOLT_AI_TOKEN=${clean(s.aiToken)}`); // hosted-tier token (used when no local key)
|
|
85
87
|
if (eff.includes("db")) {
|
|
86
88
|
out.push(`DB_DRIVER=${clean(s.dbDriver)}`);
|
|
87
89
|
if (s.dbDriver === "mongodb") {
|
|
@@ -266,6 +268,8 @@ const aiSettings = () =>
|
|
|
266
268
|
</select>
|
|
267
269
|
${() => html`<a class="small d-inline-block mb-1" href=${AI_KEY_URL[state().aiProvider] || AI_KEY_URL.anthropic} target="_blank" rel="noopener">Get a ${state().aiProvider} key → paste it below (stays server-side in .env)</a>`}
|
|
268
270
|
${field("API key", "aiKey", "sk-…")}
|
|
271
|
+
<div class="small text-muted mt-2 mb-1">— or — no key? Use the hosted tier (free, capped, then pay-as-you-go):</div>
|
|
272
|
+
${() => (state().aiToken ? html`<div class="small">Hosted token: <code>${state().aiToken.slice(0, 14)}…</code> <button class="btn btn-sm btn-link p-0 ms-1" onclick=${() => set({ aiToken: "" })}>clear</button></div>` : html`<button class="btn btn-sm btn-outline-secondary" onclick=${genToken}>Generate a free hosted token</button>`)}
|
|
269
273
|
</div>
|
|
270
274
|
</details>`;
|
|
271
275
|
|
|
@@ -304,6 +308,18 @@ async function buyCredits(amountUsd) {
|
|
|
304
308
|
status("Checkout request failed.");
|
|
305
309
|
}
|
|
306
310
|
}
|
|
311
|
+
async function genToken() {
|
|
312
|
+
status("Requesting a hosted token…");
|
|
313
|
+
try {
|
|
314
|
+
const r = await (await fetch("/setup/gen-token", { method: "POST" })).json();
|
|
315
|
+
if (r.ok && r.token) {
|
|
316
|
+
set({ aiToken: r.token });
|
|
317
|
+
status("Hosted token generated — click Apply to save it.");
|
|
318
|
+
} else status("Could not get a token: " + (r.error || "?"));
|
|
319
|
+
} catch {
|
|
320
|
+
status("Token request failed.");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
307
323
|
const items = signal({ pages: [], posts: [] });
|
|
308
324
|
const editing = signal(null); // { type, slug, title, isNew } — set only on open/save/close
|
|
309
325
|
let ed = null; // live RTEPro instance for the open editor
|
|
@@ -370,6 +370,17 @@ function startSetup() {
|
|
|
370
370
|
}
|
|
371
371
|
return;
|
|
372
372
|
}
|
|
373
|
+
// --- generate a free hosted-AI token from the gateway (self-service) ---
|
|
374
|
+
if (req.method === "POST" && p === "/setup/gen-token") {
|
|
375
|
+
res.setHeader("Content-Type", "application/json");
|
|
376
|
+
const env = readEnvFile();
|
|
377
|
+
const base = (env.VOLT_AI_GATEWAY || "https://voltjs.com/api/ai").replace(/\/api\/ai\/?$/, "");
|
|
378
|
+
fetch(base + "/api/register", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ app: env.SITE_NAME || "volt-app" }) })
|
|
379
|
+
.then((r) => r.json())
|
|
380
|
+
.then((j) => res.end(JSON.stringify(j)))
|
|
381
|
+
.catch(() => res.end(JSON.stringify({ ok: false, error: "gateway unreachable" })));
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
373
384
|
// --- AI proxy for the in-config editor (RTEPro). Uses a local provider key
|
|
374
385
|
// (BYO) when set; otherwise falls back to the voltjs.com gateway via
|
|
375
386
|
// VOLT_AI_TOKEN (free-capped, then pay-as-you-go on the host's key). The
|
|
@@ -34,6 +34,7 @@ const state = signal({
|
|
|
34
34
|
theme: current.THEME || "",
|
|
35
35
|
aiProvider: current.AI_PROVIDER || "anthropic",
|
|
36
36
|
aiKey: current.ANTHROPIC_API_KEY || current.OPENAI_API_KEY || current.GEMINI_API_KEY || "",
|
|
37
|
+
aiToken: current.VOLT_AI_TOKEN || "",
|
|
37
38
|
});
|
|
38
39
|
const set = (patch) => state({ ...state(), ...patch });
|
|
39
40
|
const toggle = (n) => state({ ...state(), addons: { ...state().addons, [n]: !state().addons[n] } });
|
|
@@ -82,6 +83,7 @@ function genEnv(s) {
|
|
|
82
83
|
const keyVar = { anthropic: "ANTHROPIC_API_KEY", openai: "OPENAI_API_KEY", gemini: "GEMINI_API_KEY" }[s.aiProvider] || "ANTHROPIC_API_KEY";
|
|
83
84
|
out.push(`${keyVar}=${clean(s.aiKey)}`);
|
|
84
85
|
}
|
|
86
|
+
if (s.aiToken) out.push(`VOLT_AI_TOKEN=${clean(s.aiToken)}`); // hosted-tier token (used when no local key)
|
|
85
87
|
if (eff.includes("db")) {
|
|
86
88
|
out.push(`DB_DRIVER=${clean(s.dbDriver)}`);
|
|
87
89
|
if (s.dbDriver === "mongodb") {
|
|
@@ -266,6 +268,8 @@ const aiSettings = () =>
|
|
|
266
268
|
</select>
|
|
267
269
|
${() => html`<a class="small d-inline-block mb-1" href=${AI_KEY_URL[state().aiProvider] || AI_KEY_URL.anthropic} target="_blank" rel="noopener">Get a ${state().aiProvider} key → paste it below (stays server-side in .env)</a>`}
|
|
268
270
|
${field("API key", "aiKey", "sk-…")}
|
|
271
|
+
<div class="small text-muted mt-2 mb-1">— or — no key? Use the hosted tier (free, capped, then pay-as-you-go):</div>
|
|
272
|
+
${() => (state().aiToken ? html`<div class="small">Hosted token: <code>${state().aiToken.slice(0, 14)}…</code> <button class="btn btn-sm btn-link p-0 ms-1" onclick=${() => set({ aiToken: "" })}>clear</button></div>` : html`<button class="btn btn-sm btn-outline-secondary" onclick=${genToken}>Generate a free hosted token</button>`)}
|
|
269
273
|
</div>
|
|
270
274
|
</details>`;
|
|
271
275
|
|
|
@@ -304,6 +308,18 @@ async function buyCredits(amountUsd) {
|
|
|
304
308
|
status("Checkout request failed.");
|
|
305
309
|
}
|
|
306
310
|
}
|
|
311
|
+
async function genToken() {
|
|
312
|
+
status("Requesting a hosted token…");
|
|
313
|
+
try {
|
|
314
|
+
const r = await (await fetch("/setup/gen-token", { method: "POST" })).json();
|
|
315
|
+
if (r.ok && r.token) {
|
|
316
|
+
set({ aiToken: r.token });
|
|
317
|
+
status("Hosted token generated — click Apply to save it.");
|
|
318
|
+
} else status("Could not get a token: " + (r.error || "?"));
|
|
319
|
+
} catch {
|
|
320
|
+
status("Token request failed.");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
307
323
|
const items = signal({ pages: [], posts: [] });
|
|
308
324
|
const editing = signal(null); // { type, slug, title, isNew } — set only on open/save/close
|
|
309
325
|
let ed = null; // live RTEPro instance for the open editor
|